五、Flutter基础之-渲染流程之三棵树

Flutter渲染流程

一、视图树

Widget不稳定,一build就要重新进行构建,如果引擎直接对它进行渲染,消耗会非常高。

所以渲染引擎并不是直接渲染WidgetTree,而是对WidgetTree对应生成的RenderTree进行渲染。

三棵树的关系
  • RenderTree中放的是RederObject对象。并不是所有Widget都会被独立渲染,只有继承了RenderObjectWidget的才会被创建RenderObject对象

如:Column/Row/Expanded -> Flex -> MultiChildRenderObjectWidget -> RenderObjectWidget

而像StatefulWidgetStatelessWidget等等是继承于Widget的,而非RenderObjectWidget,所以并不会创建RenderObject对象。

为了理解三棵树的关系,我们先来看下它们的源码:

1. Widget

可以看成一个配置数据的结构,存放有关视图渲染的配置信息,如布局、渲染属性、事件响应信息等等。Widget比较轻便,方便在页面刷新是重绘

abstract class Widget extends DiagnosticableTree {
    ...
    @protected
    @factory
    Element createElement();

    ...
}

无论那种类型的widget,都会隐式调用createElement方法,element加入Element树。

1.1 RenderObjectWidget

  • 为RenderObjectElement提供配置,遍历widget树时,会调用createElement同步widget自身配置,生成对应节点的element对象。

  • RenderObjectWidget并没有对Render做任何操作

abstract class RenderObjectWidget extends Widget {
    ...
    @override
    @factory
    RenderObjectElement createElement();
    
    @protected
    @factory
    RenderObject createRenderObject(BuildContext context);
    
    @protected
    void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
    ...
}

从上面RenderObjectWidget的源码可以看到,它继承于Widget类,重写了createElement方法,与Widget类不同的是,它额外提供了createRenderObjectupdateRenderObject两个方法,由子类实现。

如:RowColumn继承于Flex,在Flex中重写了上面方法的实现:

class Flex extends MultiChildRenderObjectWidget {
  ...
  @override
  RenderFlex createRenderObject(BuildContext context) {
    return RenderFlex(
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
      textDirection: getEffectiveTextDirection(context),
      verticalDirection: verticalDirection,
      textBaseline: textBaseline,
      clipBehavior: clipBehavior,
    );
  }
  
  @override
  void updateRenderObject(BuildContext context, covariant RenderFlex renderObject) {
    renderObject
      ..direction = direction
      ..mainAxisAlignment = mainAxisAlignment
      ..mainAxisSize = mainAxisSize
      ..crossAxisAlignment = crossAxisAlignment
      ..textDirection = getEffectiveTextDirection(context)
      ..verticalDirection = verticalDirection
      ..textBaseline = textBaseline
      ..clipBehavior = clipBehavior;
  }
  ...
}

1.2 MultiChildRenderObjectWidget

RowColumnStack这种可以添加children组件列表的,最终都继承于MultiChildRenderObjectWidget

  ...
  abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
  final List<Widget> children;

  @override
  MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
  }
  ...

这里重写了reateElement()方法,返回一继承于RenderObjectElement类的MultiChildRenderObjectElement对象。

MultiChildRenderObjectElement类的具体实现及调用时机,在后文会提及。

1.3 StatelessWidget 与 StatefulWidget

abstract class StatelessWidget extends Widget {
  ...
  @override
  StatelessElement createElement() => StatelessElement(this);
  
  @protected
  Widget build(BuildContext context);
  ...
}

abstract class StatefulWidget extends Widget {
  ...
  @override
  StatefulElement createElement() => StatefulElement(this);
  
  @protected
  @factory
  State createState();
  ...
}

继承于StatelessWidgetStatefulWidget的widget并不会创建对应的RenderObject,所以像ContainerScaffoldMaterialAppText等这样的组件,并不直接进行渲染。

Text组件为例,真正返回的是继承自MultiChildRenderObjectWidgetRichText类。

而继承于StatefulWidgetScaffold,则是在其build方法内,去添加appBarchildwidget等。

2. Element

  • 是什么?

    • 存放上下文,element其实就是BuildContext
    • 持有Widget和RenderObject,是widget的一个实例化对象
    • 每个Widget都会创建一个对应的Element
    • 通过Element遍历视图树,支撑UI结构
  • 为什么需要element?

    • widget是不可变的,element对widget树的变化做了抽象,可以只将真正变化的部分同步给RenderObject进行刷新
    • 提高渲染效率,而不是重新构建整个widget
    • 对变化前后的数据进行比较,告诉render哪些是需要重新渲染的

2.1 Element

先来看下Element类有哪几个核心方法:

abstract class Element extends DiagnosticableTree implements BuildContext {
  ...
  Element _parent;
  
  @override
  Widget get widget => _widget;
  Widget _widget;
  
  // 给element的child赋值
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {...}
  
  @mustCallSuper
  void mount(Element parent, dynamic newSlot) {...}
  
  @protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) {...}
  
  @mustCallSuper
  void unmount() {...}
  
  void rebuild() {...}
  ...
}
  • updateChild(...) 是一个很重要的方法,根据传入的三个参数的不同,会做不同的处理:
newWidget == null newWidget != null
child为null 1. Returns null. 2. 创建新element并返回
child不为null 3. 移除传入的child,返回null 4. child可能被更新,根据Widget.canUpdate(child.widget, newWidget) 方法判断返回更新后的child或创建新的element返回
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ...
    if (newWidget == null) {
      if (child != null)
        //情况3 移除子组件
        deactivateChild(child);
      //情况1
      return null;
    }
    Element newChild;
    if (child != null) {
        //情况4
        bool hasSameSuperclass = true;
        if (hasSameSuperclass && child.widget == newWidget) {
            //更新solt
            if (child.slot != newSlot)
            updateSlotForChild(child, newSlot);
            newChild = child;
        } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
            if (child.slot != newSlot)
            updateSlotForChild(child, newSlot);
            child.update(newWidget);
            newChild = child;
        } else {
            //移除并新建child element
            deactivateChild(child);
            newChild = inflateWidget(newWidget, newSlot);
        }
    } else {
      //情况2 创建element
      newChild = inflateWidget(newWidget, newSlot);
    }
    ...  
}
  • inflateWidget(...)
@protected
Element inflateWidget(Widget newWidget, dynamic newSlot) {
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        ...
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot);
        ...
        return updatedChild;
      }
    }
    final Element newChild = newWidget.createElement();
    ...
    newChild.mount(this, newSlot);
    ...
    return newChild;
}

这里可以看到,如果当前newWidget的key是GlobalKey,且在_retakeInactiveElement方法中判断到widget已经在Elementmount()中注册到了GlobalKey中,就直接复用,并调用上面的updateChild()方法,此时的newWidgetchild都不为null,所以属于情况4,调用childupdate(newWidget)方法,更新当前Element持有的Widget。

除了这些逻辑,其它的会交由子类去实现。

  • mount(...)
  @mustCallSuper
  void mount(Element parent, dynamic newSlot) {
    ...
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    if (parent != null) // Only assign ownership if the parent is non-null
      _owner = parent.owner;
    final Key key = widget.key;
    if (key is GlobalKey) {
      key._register(this);
    }
    _updateInheritance();
    assert(() {
      _debugLifecycleState = _ElementLifecycle.active;
      return true;
    }());
  }

Elementmount方法中,进行了一系列初始化的操作。

如果当前elementwidget的key是GlobalKey,会将当前element存入由GlobalKey维护的Map中。

最后,将生命周期设置为_ElementLifecycle.active

  • unmount()
  @mustCallSuper
  void unmount() {
    ...
    final Key key = _widget.key;
    if (key is GlobalKey) {
      key._unregister(this);
    }
    assert(() {
      _debugLifecycleState = _ElementLifecycle.defunct;
      return true;
    }());
  }

从GlobalKey中移除当前element,并更新生命周期为defunct

  • rebuild()
void rebuild() {
    if (!_active || !_dirty)
      return;
    ...
    performRebuild();
    ...
}

@protected
void performRebuild();

会调用performRebuild()方法,在Element中什么也没做,交由子类去实现。

接下来来看看子类ComponentElement的实现:

2.2 ComponentElement

abstract class ComponentElement extends Element {
  Element _child;
  
  ...
  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    ...
    _firstBuild();
    ...
  }
  
  void _firstBuild() {
    rebuild();
  }
  
  @override
  void performRebuild() {...}
  
  @protected
  Widget build();
}

ComponentElementmount()方法会调用到rebuild,从上面Element源码中看到,rebuild方法会最终调用performRebuild方法:

@override
void performRebuild() {
  ...
  Widget? built;
  
  try {
    ...
    built = build();
    debugWidgetBuilderValue(widget, built);
  } catch (e, stack) {
    ...
  } finally {
    _dirty = false;
  } catch (e, stack) {
    ...
  }
  try {
    _child = updateChild(_child, built, slot);
  } catch (e, stack) {
    built = ErrorWidget.builder(...);
    _child = updateChild(null, built, slot);
  }
  ...
}

可以看到,该方法中调用了build()方法来创建Widget,如果出现异常,进入catch语句创建一个ErrorWidget,就是经常看到的那个红色报错界面。

build后调用updateChild(...)来给当前Element_child赋值。

2.3 StatelessElement 和 StatefulElement

StatelessElementStatefulElementComponentElement的子类。

  • StatelessElement
class StatelessElement extends ComponentElement {
    @override
    Widget build() => widget.build(this);
    
    @override
    void update(StatelessWidget newWidget) {
        super.update(newWidget);
        assert(widget == newWidget);
        _dirty = true;
        rebuild();
    }
}

StatelessElement类内容很少,这是重新了build方法和update方法,在update内将Element标记为dirty,并调用rebuild()

  • StatefulElement
class StatefulElement extends ComponentElement {

  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    ...
    _state._element = this;
    ...
    _state._widget = widget;
    ...
   }
   
  
  @override
  void _firstBuild() { ... }
  
  @override
  void update(StatefulWidget newWidget) { ... }
  
  @override
  void unmount() { ... }
}

StatefulElement的构造方法中,将当前Elementwidget存放在了state中。

先来看下这几个方法都做了什么

_firstBuild()

@override
void _firstBuild() {
  ...
  final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
  ...
  state._debugLifecycleState = _StateLifecycle.initialized;
  ...
  state.didChangeDependencies();
  ...
  state._debugLifecycleState = _StateLifecycle.ready;
  super._firstBuild();
}

从上面ComponentElement源码看到,在mount(...)方法被调用时,会调用_firstBuild()方法,StatefulElement中对该方法进行了重新,并进行了一系列初始化、state生命周期的控制。

并且调用 state.initState() 方法。

update(...)

@override
void update(StatefulWidget newWidget) {
    super.update(newWidget);
    ...
    final StatefulWidget oldWidget = state._widget!;
    _dirty = true;
    state._widget = widget as StatefulWidget;
    ...
    final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
    ...
    rebuild();
}

这里主要调用了state.didUpdateWidget(oldWidget)

2.3 RenderObjectElement

所有继承自RenderObjectWidget的Widget会创建在createElement()时返回一个RenderObjectElement类型的element。

下面看下RenderObjectElement类的核心方法:

abstract class RenderObjectElement extends Element {
    ...
    @override
    RenderObject get renderObject => _renderObject!;
    RenderObject? _renderObject;
    ...
    void mount(Element? parent, Object? newSlot) {...}
    
    @override
    void update(covariant RenderObjectWidget newWidget) {...}
    
    @override
    void attachRenderObject(Object? newSlot) {...}
    
    @override
    void detachRenderObject() {...}
    
    @protected
    @mustCallSuper
    void insertChildRenderObject(covariant RenderObject child, covariant Object? slot) {...}
    
    @protected
    void insertRenderObjectChild(covariant RenderObject child, covariant Object? slot) {...}
    
    @protected
    @mustCallSuper
    void moveChildRenderObject(covariant RenderObject child, covariant Object? slot)
    
    @protected
    @mustCallSuper
    void removeChildRenderObject(covariant RenderObject child) 
    ...
}
  • mount(...)
void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    ...
    _renderObject = widget.createRenderObject(this);
    ...
    attachRenderObject(newSlot);
    _dirty = false;
}

与之前子类的mount()方法不同的是,此处会调用widget.createRenderObject(this)去创建对应的RenderObject对象,并调用attachRenderObject()方法对RenderObject进行进一步的操作,在此之后,_dirty就被置为了false,下面看看attachRenderObject中做了些什么:

  • attachRenderObject(newSlot)
  @override
  void attachRenderObject(Object? newSlot) {
    _slot = newSlot;
    //查找祖先节点的`RenderObjectElement`
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    //调用祖先节点的`insertRenderObjectChild`方法,把当前`renderObject`插入
    _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
    //查找最近的ParentDataElement
    final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
    if (parentDataElement != null)
      //将父节点的数据通过ParentData存储在子节点中
      _updateParentData(parentDataElement.widget);
  }
  1. 查找祖先节点的RenderObjectElement,调用祖先节点的insertRenderObjectChild方法,把当前renderObject插入。
    • insertRenderObjectChild方法由子类(如:MultiChildRenderObjectElementSingleChildRenderObjectElement)来实现。最终都会调用RenderObjectadoptChild(child)方法。
  1. 查找最近的ParentDataElement,将父节点的数据通过ParentData存储在子节点中。如: StackPosition

3. RenderObject

  • 是什么?
    • RenderObjectAbstractNode的子类,同时实现了HitTestTarget接口来处理点击事件。
    • 负责真正的渲染工作
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
    ...
}

下面来先来看下其父类AbstractNode

3.1 AbstractNode

AbstractNode是对树节点的抽象类,下面看下它的一些重要方法:

class AbstractNode {
  int get depth => _depth;
  int _depth = 0;
  
  @protected
  void redepthChild(AbstractNode child) {...}
  
  @mustCallSuper
  void attach(covariant Object owner) {...}
  ...
  AbstractNode _parent;
  
  @protected
  @mustCallSuper
  void adoptChild(covariant AbstractNode child) {...}
  
  @protected
  @mustCallSuper
  void dropChild(covariant AbstractNode child) {...}
}

AbstractNode定义了树的深度、父节点、是否attached。提供了一些方法可供子类重写实现,如上面RenderObjectElementattachRenderObject()时会调用adoptChild(...)方法:

adoptChild(AbstractNode child)

  @protected
  @mustCallSuper
  void adoptChild(covariant AbstractNode child) {
    ...
    child._parent = this;
    if (attached)
      child.attach(_owner!);
    redepthChild(child);
  }

再看下RenderObject中的实现

3.2 RenderObject

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
    ...
    
    @override
    void adoptChild(RenderObject child) {...}

    @override
    PipelineOwner? get owner => super.owner as PipelineOwner?;
    
    @override
    void attach(PipelineOwner owner) {...}
    
    void markNeedsLayout() {...}
    
    void layout(Constraints constraints, { bool parentUsesSize = false }) {...}
    
    void paint(PaintingContext context, Offset offset) {...}

    ...
}

adoptChild(...)

  @override
  void adoptChild(RenderObject child) {
    ...
    setupParentData(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
  }

重写了父类的adoptChild方法,子类在更改了其子列表时会调用,这里面有个比较重要的方法:markNeedsLayout()

markNeedsLayout()

void markNeedsLayout() {
    ...
    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } else {
      _needsLayout = true;
      if (owner != null) {
        ...
        owner!._nodesNeedingLayout.add(this);
        owner!.requestVisualUpdate();
      }
    }
  }
  • _relayoutBoundary: 重绘边界
    • 当重绘边界为当前RenderObject时,标记_needsLayout为true;添加当前RenderObjectPipelineOwner_nodesNeedingLayout列表中。
      • PipelineOwner为该节点的所有者,所有子树拥有同一个owner。
    • 如果边界为父节点,则调用markParentNeedsLayout() -> 父节点的markNeedsLayout()

PipelineOwner 渲染管线

PipelineOwner类管理着渲染管道,并提供一些接口来驱动渲染管道。PipelineOwner存储着需要渲染节点的RenderObject

要刷新管道,要依次调用:flushLayout()flushCompositingBits()flushPaint()flushSemantics()

Element刷新流程中,会进入WidgetsBindingdrawFrame()方法中,也印证了PipelineOwner的流程:

  @protected
  void drawFrame() {
    assert(renderView != null);
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    if (sendFramesToEngine) {
      renderView.compositeFrame();       
      pipelineOwner.flushSemantics();
      _firstFrameSent = true;
    }
  }
  • flushLayout()
void flushLayout() {
    ...
    try {
      while (_nodesNeedingLayout.isNotEmpty) {
        final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
        _nodesNeedingLayout = <RenderObject>[];
        for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
          if (node._needsLayout && node.owner == this)
            node._layoutWithoutResize();
        }
      }
    } finally {
      _debugDoingLayout = false;
      ...
    }
  }

深度优先遍历_nodesNeedingLayout列表,调用RenderObject_layoutWithoutResize方法。

RenderObject_layoutWithoutResize方法内,必须调用layout()方法:

void layout(Constraints constraints, { bool parentUsesSize = false }) {
    ...
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
    }
    ...
    _relayoutBoundary = relayoutBoundary;
    ...
    if (sizedByParent) {
      ...
        performResize();
      ...
    }
    ...
      performLayout();
    ...
    _needsLayout = false;
    markNeedsPaint();
  }

在子类的performResize()时,确定了布局的大小,之后再去调用markNeedsPaint():如果isRepaintBoundary为false,则一直向父节点遍历,直到为true停止,并将当前render加入owner的_nodesNeedingPaint列表中。

  • flushCompositingBits()
void flushCompositingBits() {
    ...
    _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
    for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
      if (node._needsCompositingBitsUpdate && node.owner == this)
        node._updateCompositingBits();
    }
    _nodesNeedingCompositingBitsUpdate.clear();
    ...
  }

在该方法中,更新视图层合成信息。

  • flushPaint()
void flushPaint() {
    ...
    _debugDoingPaint = true;
    ...
    final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = <RenderObject>[];
      // Sort the dirty nodes in reverse order (deepest first).
      for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
        assert(node._layer != null);
        if (node._needsPaint && node.owner == this) {
          if (node._layer!.attached) {
            PaintingContext.repaintCompositedChild(node);
          } else {
            node._skippedPaintingOnLayer();
          }
        }
      }
    ...
    _debugDoingPaint = false;
}

同样是深度优先遍历_nodesNeedingPaint列表,调用每个节点的paint方法进行重绘,生成Layer。

Layer中保存了绘制结果,来避免不必要的重绘,只有遇到isRepaintBoundary为true,即为重绘边界时,才会生成新的Layer。

PaintingContext为进行绘制的对象,它会持有一个Canvas,对node进行重绘。

  • 在此之后,就能得到Layer树,之后就是渲染引擎做的事了。在上面drawframe()中看到,调用renderView.compositeFrame()来把bits数据传给引擎。

二、流程总结

从应用启动进入RunApp后的流程大致如下:

widgetCreate.png

Flutter Framework:

  1. 创建RootWidget - RenderObjectToWidgetAdapter,创建对应的_renderViewElement

  2. 调用elementmount()方法。在ComponentElement中,最终会调用performRebuild()方法 —— Stateless执行widgetbuild()方法; StatefulElement调用state.build

    updateChild(...)根据传入参数不同,有四种情况,返回Element? _child

    当child element为null时,调用nflateWidget(newWidget, newSlot)。进入循环创建子element的过程。

  3. 若当前element继承于RenderObjectElement, 如常见的SingleChildRenderObjectElementMultiChildRenderObjectElementmount()方法处理执行上述流程外,还会创建RenderObject,执行attachRenderObject(newSlot)去构建render树;

  4. markNeedsLayout() - 不断找重回边界,找到后,将当前节点的RenderObject添加到PipelineOwner维护的_nodesNeedingLayout列表中;

  5. 执行EnginescheduleFrame()方法。

Engine:

  1. 执行scheduleFrame()后注册VSync信号回调
  2. Engine触发Flutter FrameworkRenderBindingdrawFrame()方法;

Flutter Framework:

1.drawFrame()后依次经过BuildLayoutPaint后生成LayerTree

Engine:

  1. Skia将LayerTree数据加工成GUP数据
  2. 通过OpenGL交给GPU进行渲染

三、iOS与Fuller渲染对比

1. iOS渲染流程

第一步,更新视图树,同步更新图层树。

第二步,CPU 计算要显示的内容、图像解码转换。当runloop 在BeforeWaitingExit时,会通知注册的监听,然后对图层打包,打完包后,将打包数据发送给一个独立负责渲染的进程Render Server`。

第三步,Render Server 将数据反序列化,得到图层树。按照图层树中图层顺序、RGBA 值、图层 frame 过滤图层中被遮挡的部分,过滤后将图层树转成渲染树,渲染树的信息会转给 OpenGL ES/Metal。

第四步,Render Server 会调用 GPU,GPU 开始进行顶点着色器、形状装配、几何着色器、光栅化、片源着色器、测试与混合六个阶段。

第五步,将GPU渲染结果放到帧缓冲区,当下个Vsync信号时,从帧缓冲区取出放到屏幕。

2. flutter

flutter_vsync.png
  • Flutter由Skia绘制引擎提供提供图形绘制能力,向GPU提供数据。(替代了iOS中的Core Graphics、Core Animation、Core Text)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容