Flutter系列七:Flutter布局和绘制流程浅析

我们前面介绍了StatelessWidgetStatefulWidget,它们只是对其他Widget进行组合,不具备自定义绘制的能力。在需要绘制内容的场景下,我们要使用RenderObjectWidget,因为RenderObjectWidget创建的RenderObject负责布局和绘制的功能。

本文将以RenderObject为起点,梳理下Flutter的布局和绘制流程的逻辑。

RenderObject

RenderObject渲染树Render Tree的一个节点,主要负责布局绘制

Flutter设计了重要的三棵树Widget Tree - Element Tree - RenderObject Tree。示例如下:

image

图片引用来源

RenderObjectWidget实例化的RenderObjectElement会创建 RenderObject, 所有的RenderObject会组成一颗RenderObject Tree

abstract class RenderObject extends AbstractNode implements HitTestTarget {}

RenderObject继承自AbstractNode, AbstractNode是对树的节点的抽象:

class AbstractNode {
    // 1
    int get depth => _depth;
    int _depth = 0;
    void redepthChild(AbstractNode child) {}
    
    // 2
    Object? get owner => _owner;
    Object? _owner;
    
    void attach(covariant Object owner) {
        _owner = owner;
    }
    void detach() {
        _owner = null;
    }
    
    // 3
    AbstractNode? get parent => _parent;
    AbstractNode? _parent;
  
    void adoptChild(covariant AbstractNode child) {
        child._parent = this;
        if (attached)
          child.attach(_owner!);
        redepthChild(child);
    }
    
    void dropChild(covariant AbstractNode child) {
        child._parent = null;
        if (attached)
          child.detach();
    }
}
  • AbstractNode提供了三个属性和几个重要的方法:
  1. 节点深度depth属性和计算节点深度redepthChild()方法;
  2. owner和对应的关联attach()和取消关联detach()方法;
  3. parent父节点;
  4. 挂载子节点adoptChild()和卸载子节点dropChild()方法。
abstract class RenderObject extends AbstractNode implements HitTestTarget {
    // 1
    ParentData? parentData;
    
    // 2
    Constraints _constraints;
    
    // 3
    RenderObject? _relayoutBoundary;
    
    // 众多方法...
}
  • RenderObject自身也有几个重要的属性:
  1. parentData父节点的插槽,父节点的一些信息可以放置在这里面供子节点使用;
  2. _constraints为父节点提供的约束;
  3. _relayoutBoundary是需要重新布局的边界。
  • RenderObject的方法和Android的View非常类似:
功能 RenderObject View
布局 performLayout() measure()/measure()
绘制 paint() draw()
请求布局 markNeedsLayout() requestLayout()
请求绘制 markNeedsPaint() invalidate()
父节点/View parent getParent()
添加子节点/View adoptChild() addView()
移除子节点/View dropChild() removeView()
关联owner/Window attach() onAttachedToWindow()
取消关联owner/Window detach() onDetachedFromWindow()
事件 hitTest() onTouch()
屏幕旋转 rotate() onConfigurationChanged()
参数 parentData mLayoutParams
  • RenderObject还有一个特点 --- 它定义了布局/绘制协议,但并没定义具体布局/绘制模型

定义了布局/绘制协议就是指继承RenderObject的子类必须要实现一些方法,譬如performLayoutpaint等;没定义具体布局/绘制模型是指没有限定使用什么坐标系,子节点可以有0个、1个还是多个等。

  • Flutter提供了RenderBoxRenderSlive两个子类,他们分别对应简单的2D笛卡尔坐标模型和滚动模型。
RenderObject子类 Constraints ParentData
RenderBox BoxConstraints BoxParentData
RenderSlive SliverConstraints SliverLogicalParentData

一般情况下我们不需要直接使用RenderObject,使用RenderBoxRenderSlive这两个子类就能满足需求。

SchedulerBinding.handleDrawFrame()

我们介绍这个方法是为了介绍每次刷新的工作流程,这样有助于我们更好的理解RenderObject的相关内容。

Flutter启动流程分析那篇文章中,我们提到过window.scheduleFrame()Native platform发起一个刷新视图的请求后,Flutter Engine会在适当的时候调用SchedulerBinding_handleDrawFrame方法。

void handleDrawFrame() {
    try {
      // PERSISTENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (final FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp!);
    } finally {
    }
}

handleDrawFrame中执行了回调函数数组persistentCallbacks中所有的回调函数。其中就包括RendererBinding中的_handlePersistentFrameCallback方法:

<!-- RendererBinding -->
void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
    _scheduleMouseTrackerUpdate();
}

这里的drawFrame方法是调用的父类WidgetsBinding的方法:

<!-- WidgetsBinding -->
void drawFrame() {
    try {
      // 1
      if (renderViewElement != null)
        buildOwner!.buildScope(renderViewElement!);
      // 2
      super.drawFrame();
      // 3
      buildOwner!.finalizeTree();
    } finally {
     
    }
}

此方法代表的含义:

  1. buildOwner!.buildScope(renderViewElement!)执行的是Widgetbuild任务,这其中就包括StatelessWidgetStatefulWidgetRenderObjectWidget
  2. 调用WidgetsBindingdrawFrame方法;
  3. 卸载非激活状态的Element

WidgetsBindingdrawFrame方法中则执行了布局和绘制等操作。

void drawFrame() {
    assert(renderView != null);
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    if (sendFramesToEngine) {
      renderView.compositeFrame(); // this sends the bits to the GPU
      pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
      _firstFrameSent = true;
    }
}
flow

上面的buid/layout/paint等都和RenderObject息息相关,我们将在接下来的章节中详细介绍。

Build

接下来我们就来看看buildScope方法触发的RenderObjectWidget的构建过程。

Element inflateWidget(Widget newWidget, dynamic newSlot) {
    // 1
    final Element newChild = newWidget.createElement();
    // 2
    newChild.mount(this, newSlot);
    return newChild;
}

inflateWidget方法主要作用:

  1. 先通过createElement方法根据Widget创建对应的Element
  2. 然后新建的Element调用mount方法,将自己挂载到Element Tree上,位置是父ElementnewSlot这个插槽。

createElement

abstract class RenderObjectWidget extends Widget {
    @factory
    RenderObjectElement createElement();
}

RenderObjectWidgetcreateElement方法是工厂方法,真正的实现方法在子类里面。

RenderObjectWidget的子类对应的Element总结:

分类 Widget Element
根节点 RenderObjectToWidgetAdapter RootRenderObjectElement
具有多个子节点 MultiChildRenderObjectWidget MultiChildRenderObjectElement
具有一个子节点点 SingleChildRenderObjectWidget SingleChildRenderObjectElement
叶子节点 LeafRenderObjectWidget LeafRenderObjectElement

代码如下:

abstract class LeafRenderObjectWidget extends RenderObjectWidget {
  @override
  LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}

abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
  @override
  SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
}

abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
  @override
  MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}

class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
  @override
  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);

  @override
  RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
}

mount

我们来看RenderObjectElementmount方法实现:

void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
}
  1. super.mount的作用主要是记录下parent,slotdepth等值;
  2. widget.createRenderObject创建了一个renderObject
  3. attachRenderObject就是将这个parentData挂载到RenderObject Tree上,并且更新RenderObjectparentData
void attachRenderObject(dynamic newSlot) {
    _slot = newSlot;
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
    final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
    if (parentDataElement != null)
      _updateParentData(parentDataElement.widget);
}

insertRenderObjectChild

renderObject通过insertRenderObjectChild方法挂载到RenderObject Tree上,那具体的实现是如何实现的呢?

能实现挂载RenderObject的只能是SingleChildRenderObjectElementMultiChildRenderObjectElement。我们分别来看看:

SingleChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, dynamic slot) {
    final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
    renderObject.child = child;
}

child进行赋值:

set child(ChildType? value) {
    if (_child != null)
      dropChild(_child!);
    _child = value;
    if (_child != null)
      adoptChild(_child!);
}

如果已经有_child先将其从卸载,然后将新的Child挂载上。

void dropChild(RenderObject child) {
    child._cleanRelayoutBoundary();
    child.parentData!.detach();
    child.parentData = null;
    super.dropChild(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
}
void adoptChild(RenderObject child) {
    setupParentData(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
}

这两个方法主要是对_childparentData重新赋值,然后通过markNeedsLayoutmarkNeedsCompositingBitsUpdatemarkNeedsSemanticsUpdate标记需要重新布局,需要合成和语义的更新。

MultiChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
    final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject;
    renderObject.insert(child, after: slot.value?.renderObject);
}
void insert(ChildType child, { ChildType? after }) {
    adoptChild(child);
    _insertIntoChildList(child, after: after);
}

MultiChildRenderObjectElement中的实现方式类似,只是这次不是简单的赋值,而是将child添加到Render Tree中去,然后进行各种标记。

_insertIntoChildList方法添加的逻辑如下:

  • 依附的兄弟节点为空,插入在第一个子节点;
  • 依附的兄弟节点没有相关联的下一个兄弟节点,插入在兄弟节点队尾;
  • 依附的兄弟节点有相关联的下一个兄弟节点,插入在兄弟节点中间。

inflateWidget递归

由于SingleChildRenderObjectWidgetMultiChildRenderObjectWidget含有子节点,所以需要对子Widget进行构建。

<!-- SingleChildRenderObjectWidget -->
void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _child = updateChild(_child, widget.child, null);
}

Element? updateChild(Element? child, Widget? newWidget, dynamic newSlot) {
    final Element newChild;
    // ... 省略Widget更新的逻辑
    newChild = inflateWidget(newWidget, newSlot);
    return newChild;
}
<!-- MultiChildRenderObjectElement -->
void mount(Element? parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
    Element? previousChild;
    for (int i = 0; i < children.length; i += 1) {
      final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
      children[i] = newChild;
      previousChild = newChild;
    }
    _children = children;
}

这样,接下来的操作就进入了递归流程了,和上面介绍的流程内容一模一样了。

流程示意图:

RenderObjectWidget Build

markNeedsLayout

我们上面看到了,adoptChildadoptChild的方法中都调用了markNeedsLayout的相关内容:

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {

    bool _needsLayout = true;

    RenderObject? _relayoutBoundary;
    
    void markNeedsLayout() {
        // 1
        if (_needsLayout) {
          return;
        }
        // 2
        if (_relayoutBoundary != this) {
          markParentNeedsLayout();
        } else {
          // 3
          _needsLayout = true;
          if (owner != null) {
            owner!._nodesNeedingLayout.add(this);
            owner!.requestVisualUpdate();
          }
        }
    }
}

RenderObject_needsLayout属性,标记是否需要重新布局,还有一个_relayoutBoundary布局边界属性,表示开始重新布局的节点,这样就不需要每次整个渲染树的节点都进行重新布局。

markNeedsLayout代表的含义:

  1. 如果已经标记_needsLayout,直接返回;
  2. 如果_relayoutBoundary布局边界不是自身,让父节点递归调用markNeedsLayout方法;
  3. 如果_relayoutBoundary布局边界是自身,标记_needsLayout, 并将自身加入到PipelineOwner_nodesNeedingLayout列表中,等待PipelineOwner进行重新布局;
  4. 请求PipelineOwner进行更新。

您可能会有疑问_relayoutBoundary是在什么时候赋值的?有两个地方赋值:

  1. 第一次布局的时候,_relayoutBoundary会被标记为RenderView,即自身,然后从根节点进行布局;
void scheduleInitialLayout() {
    _relayoutBoundary = this;
    owner!._nodesNeedingLayout.add(this);
}
  1. layout()方法中RenderObject也会重新标记_relayoutBoundary,一般情况下也是自身。
void layout(Constraints constraints, { bool parentUsesSize = false }) {
    // ...
    _relayoutBoundary = relayoutBoundary;
}

markNeedsCompositingBitsUpdate

bool _needsCompositingBitsUpdate = false;

void markNeedsCompositingBitsUpdate() {
    if (_needsCompositingBitsUpdate)
      return;
    _needsCompositingBitsUpdate = true;
    if (parent is RenderObject) {
      final RenderObject parent = this.parent! as RenderObject;
      if (parent._needsCompositingBitsUpdate)
        return;
      if (!isRepaintBoundary && !parent.isRepaintBoundary) {
        parent.markNeedsCompositingBitsUpdate();
        return;
      }
    }
    if (owner != null)
      owner!._nodesNeedingCompositingBitsUpdate.add(this);
}

RenderObject_needsCompositingBitsUpdate属性,标记是否需要合成。

markNeedsCompositingBitsUpdate的逻辑如下:

  1. 如果已经标记_needsCompositingBitsUpdate,直接返回;
  2. 如果未标记_needsCompositingBitsUpdate先标记,然后标记父节点或者向父类递归调用markNeedsCompositingBitsUpdate直到标记成功为止;
  3. 将自身加入到PipelineOwner_nodesNeedingCompositingBitsUpdate列表中。

结果就是将isRepaintBoundary这个节点下的所有节点都标记为_needsCompositingBitsUpdate,然后加入到PipelineOwner_nodesNeedingCompositingBitsUpdate列表中。

flushLayout

前面所有的逻辑只能算是buildScope方法触发的Build阶段。接下来我们就进入了Layout阶段了。

<!-- PipelineOwner -->
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 {
    }
}

PipelineOwnerflushLayout其实很简单,让_nodesNeedingLayout中的所有RenderObject按照广度优先遍历调用_layoutWithoutResize方法。

<!-- RenderObject -->
void _layoutWithoutResize() {
    try {
      performLayout();
    } catch (e, stack) {
    }
    _needsLayout = false;
    markNeedsPaint();
}

我们来看看_ScaffoldLayout中的实现:

void performLayout() {
    size = _getSize(constraints);
    delegate._callPerformLayout(size, firstChild);
}

void _callPerformLayout(Size size, RenderBox? firstChild) {
    performLayout(size);
}

void performLayout(Size size) {
    layoutChild(_ScaffoldSlot.body, bodyConstraints);
    positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop));
}

Size layoutChild(Object childId, BoxConstraints constraints) {
    child!.layout(constraints, parentUsesSize: true);
    return child.size;
}

void positionChild(Object childId, Offset offset) {
    final MultiChildLayoutParentData childParentData = child!.parentData! as MultiChildLayoutParentData;
    childParentData.offset = offset;
}

根据一系列调用,生成一个BoxConstraints传递给每个子节点,子节点调用layout() 进行测量和布局。

void layout(Constraints constraints, { bool parentUsesSize = false }) {
    // 1
    RenderObject? relayoutBoundary;
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
    }
    
    // 2
    if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
      return;
    }
    // 3
    _constraints = constraints;
    if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
      visitChildren(_cleanChildRelayoutBoundary);
    }
    _relayoutBoundary = relayoutBoundary;
    
    // 4
    if (sizedByParent) {
      try {
        performResize();
        
      } catch (e, stack) {
      }
      
    }
    try {
      // MultiChildLayoutDelegate --- performLayout & _callPerformLayout & performLayout & child!.layout
      // 5 
      performLayout();
      markNeedsSemanticsUpdate();
      
    } catch (e, stack) {
    }
 
    _needsLayout = false;
    markNeedsPaint();
}
  1. 首先根据!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject的条件进行_relayoutBoundary的计算,一般情况下会指向自身;

parentUsesSize表示是否父节点的大小依赖子节点,sizedByParent表示大小由父类决定,constraints.isTight表示大小是固定的。

  1. 根据!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary确实定是否需要重新布局,不需要直接返回;
  2. 记录下_constraints;
  3. 如果依赖父节点的大小,则根据_constraints计算出size尺寸, ;
  4. performLayout根据根据_constraints计算出size尺寸,然后调用子类的layout方法。
总结:

performLayout的逻辑就是通过layout方法将Constraints逐步往下传递,得到Size逐步向上传递,然后父节点通过给parentData赋值确定对子节点的位置摆放。

flushLayout

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();
  }

遍历_nodesNeedingCompositingBitsUpdate中的每个RenderObject,然后调用_updateCompositingBits方法。

void _updateCompositingBits() {
    if (!_needsCompositingBitsUpdate)
      return;
    final bool oldNeedsCompositing = _needsCompositing;
    _needsCompositing = false;
    visitChildren((RenderObject child) {
      child._updateCompositingBits();
      if (child.needsCompositing)
        _needsCompositing = true;
    });
    if (isRepaintBoundary || alwaysNeedsCompositing)
      _needsCompositing = true;
    if (oldNeedsCompositing != _needsCompositing)
      markNeedsPaint();
    _needsCompositingBitsUpdate = false;
}

这个方法就是找到isRepaintBoundarytrue的节点及其父节点,将它们的_needsCompositing为true设置为true;

isRepaintBoundary

上面提到的isRepaintBoundaryRenderObject的一个属性,默认是false。表示的是否需要独立渲染。

<!-- RenderObject -->
bool get isRepaintBoundary => false;

如果需要独立渲染则需要覆盖这个值为true,例如RenderView的值就为true

<!-- RenderView -->
@override
bool get isRepaintBoundary => true;

flushPaint

按照逻辑flushPaint之前应该先会调用markNeedsPaint,我们回过头来看看发现确实如此,有很多地方都频繁的调用的markNeedsPaint,譬如_layoutWithoutResize,layout,_updateCompositingBits等方法中都有出现,只是前面我们特意忽略了这个逻辑。

void markNeedsPaint() {
    if (_needsPaint)
      return;
    _needsPaint = true;
    if (isRepaintBoundary) {
      if (owner != null) {
        owner!._nodesNeedingPaint.add(this);
        owner!.requestVisualUpdate();
      }
    } else if (parent is RenderObject) {
      final RenderObject parent = this.parent! as RenderObject;
      parent.markNeedsPaint();
    } else {
      if (owner != null)
        owner!.requestVisualUpdate();
    }
}
  1. 如果isRepaintBoundarytrue, 则加入到_nodesNeedingPaint数组中,然后请求界面更新;
  2. 如果isRepaintBoundaryfalse,则向父节点遍历;
  3. 如果到了根节点,就直接请求界面更新;

我们接下来看看flushPaint的代码:

void flushPaint() {
    try {
      final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = <RenderObject>[];
      for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
        if (node._needsPaint && node.owner == this) {
          if (node._layer!.attached) {
            PaintingContext.repaintCompositedChild(node);
          } else {
            node._skippedPaintingOnLayer();
          }
        }
      }
    } finally {
    }
}

从下往上遍历_nodesNeedingPaint数组,然后从上往下进行绘制。

接下来我们看看是如何绘制的:

static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
    _repaintCompositedChild(
      child,
      debugAlsoPaintedParent: debugAlsoPaintedParent,
    );
}

static void _repaintCompositedChild(
    RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext? childContext,
  }) {
    OffsetLayer? childLayer = child._layer as OffsetLayer?;
    if (childLayer == null) {
      child._layer = childLayer = OffsetLayer();
    } else {
      childLayer.removeAllChildren();
    }
    childContext ??= PaintingContext(child._layer!, child.paintBounds);
    // 重点
    child._paintWithContext(childContext, Offset.zero);

    childContext.stopRecordingIfNeeded();
}

PaintingContext的类方法repaintCompositedChild接收了RenderObject对象,最后结果是这个RenderObject对象调用_paintWithContext方法,参数是PaintingContext对象和偏移量Offset

void _paintWithContext(PaintingContext context, Offset offset) {
    
    if (_needsLayout)
      return;

    _needsPaint = false;
    try {
      paint(context, offset);
    } catch (e, stack) {
      
    }
}
<!-- PaintingContext -->
Canvas? _canvas;

_paintWithContext方法调用的是RenderObject子类对象的paint(PaintingContext context, Offset offset)进行绘制,绘制在PaintingContextCanvas上。

总结

本文主要分析了RendObjectBuildLayoutPaint等相关内容,后续继续分析其他相关内容。

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

推荐阅读更多精彩内容