Flutter運行原理篇之paint重繪原理

哈羅大家好钥平,好久不見,隔了很多天了好久沒更新了姊途,最近事情太多了而且狀態(tài)也不是很好一直沒有更新涉瘾,趁著今天公司年會沒有任務(wù),大家都在組隊王者榮耀吭净,我就偷個閑睡汹,今天我們繼續(xù)來講解Flutter運行原理篇的內(nèi)容,也算是我們新年的第一篇吧寂殉,Let ’s go
上一篇講到了《Flutter運行原理篇之layout布局的過程》最后面的彩蛋我們提到了會講到paint重繪囚巴,所以今天我們就來講講《Flutter運行原理篇之paint重繪的原理》,讓我們愉快的開始吧

在開始之前我們先要簡單的了解幾個概念:

  • Canvas:封裝了Flutter Skia各種繪制指令友扰,比如畫線彤叉、畫圓、畫矩形等指令村怪,這個與我們在移動端以及Web端的Canvas概率上沒有本質(zhì)的區(qū)別秽浇,Canvas 繪制完成后,通過 PictureRecorder 獲取繪制產(chǎn)物甚负,然后將其保存在 Layer 中

  • layout:也稱作圖層柬焕,可以分為容器類和繪制類兩種;可以理解為繪制就是在圖層上面進行的梭域,比如調(diào)用 Canvas 的繪制 API 后斑举,相應(yīng)的繪制產(chǎn)物被保存在 PictureLayer.picture 對象中, 最常見的layout包括:

  1. OffsetLayer:根 Layer病涨,它繼承自ContainerLayer富玷,而ContainerLayer繼承自 Layer 類,我們將直接繼承自ContainerLayer 類的 Layer 稱為容器類Layer,容器類 Layer 可以添加任意多個子Layer赎懦。
  2. PictureLayer:保存繪制產(chǎn)物的 Layer雀鹃,它直接繼承自 Layer 類。我們將可以直接承載(或關(guān)聯(lián))繪制結(jié)果的 Layer 稱為繪制類 Layer励两,相應(yīng)的繪制產(chǎn)物PictureRecorder被保存在 PictureLayer.picture 對象中
  • RepaintBoundary:邊界節(jié)點Widget黎茎,顧名思義也就是繪制的邊界節(jié)點,繪制只能最多影響到離他最近的一個邊界節(jié)點当悔,邊界之外的Widget并不受影響

  • isRepaintBoundary:bool類型工三,意思是申明Widget自己是邊界節(jié)點,作用與上面異曲同工

好了先鱼,了解了這幾個概念以后我們再來看看paint的步驟,與build和layout的內(nèi)容一樣奸鬓,我們還是分為兩步來講解paint的重繪的過程焙畔,分別是第一次重繪,與更新widget以后的重繪串远,讓我們先來看看第一次重繪的內(nèi)容:

第一次重繪肯定是從根RenderObject也就是RenderView開始:

RenderView.paint

@override
void paint(PaintingContext context, Offset offset) {
  if (child != null)
    context.paintChild(child!, offset);
}

PaintingContext.paintChild

void paintChild(RenderObject child, Offset offset) {
  assert(() {
    if (debugProfilePaintsEnabled)
      Timeline.startSync('${child.runtimeType}', arguments: timelineArgumentsIndicatingLandmarkEvent);
    debugOnProfilePaint?.call(child);
    return true;
  }());

  if (child.isRepaintBoundary) {            //判斷是否邊界節(jié)點宏多,因為RenderView肯定是邊界節(jié)點
    stopRecordingIfNeeded();
    _compositeChild(child, offset);
  } else {
    child._paintWithContext(this, offset);
  }

  assert(() {
    if (debugProfilePaintsEnabled)
      Timeline.finishSync();
    return true;
  }());
}

RenderView.isRepaintBoundary

@override
bool get isRepaintBoundary => true;  //屬性為true

看看RenderView的isRepaintBoundary屬性,說明RenderView肯定是邊界節(jié)點澡罚,上面If里面判斷了是否邊界節(jié)點伸但,因為RenderView肯定是邊界節(jié)點,然后往下進行

void stopRecordingIfNeeded() {
  if (!_isRecording)
    return;
  assert(() {
    if (debugRepaintRainbowEnabled) {
      final Paint paint = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 6.0
        ..color = debugCurrentRepaintColor.toColor();
      canvas.drawRect(estimatedBounds.deflate(3.0), paint);
    }
    if (debugPaintLayerBordersEnabled) {
      final Paint paint = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1.0
        ..color = const Color(0xFFFF9800);
      canvas.drawRect(estimatedBounds, paint);
    }
    return true;
  }());
  _currentLayer!.picture = _recorder!.endRecording();
  _currentLayer = null;
  _recorder = null;
  _canvas = null;
}

stopRecordingIfNeeded 在下面我講解留搔,這里先略過

void _compositeChild(RenderObject child, Offset offset) {
  assert(!_isRecording);
  assert(child.isRepaintBoundary);
  assert(_canvas == null || _canvas!.getSaveCount() == 1);

  // Create a layer for our child, and paint the child into it.
  if (child._needsPaint) {
    repaintCompositedChild(child, debugAlsoPaintedParent: true);
  } else {
    assert(() {
      // register the call for RepaintBoundary metrics
      child.debugRegisterRepaintBoundaryPaint(
        includedParent: true,
        includedChild: false,
      );
      child._layer!.debugCreator = child.debugCreator ?? child;
      return true;
    }());
  }
  assert(child._layer is OffsetLayer);
  final OffsetLayer childOffsetLayer = child._layer! as OffsetLayer;
  childOffsetLayer.offset = offset;
  appendLayer(child._layer!);
}

這個函數(shù)里面有3個地方需要注意的:

  1. 繪制孩子節(jié)點時更胖,如果遇到邊界節(jié)點且當(dāng)其不需要重繪(_needsPaint 為 false) 時,會直接復(fù)用該邊界節(jié)點的 layer隔显,而無需重繪却妨!這就是邊界節(jié)點能跨 frame 復(fù)用的原理(可以理解為重繪如果遇到的是邊界節(jié)點可以復(fù)用,非邊界節(jié)點則需要重繪括眠,理解這個非常重要彪标,務(wù)必仔細(xì)體會)。
  2. 因為邊界節(jié)點的layer類型是ContainerLayer掷豺,所以是可以給它添加子節(jié)點捞烟。
  3. 注意是將當(dāng)前邊界節(jié)點的layer添加到父邊界節(jié)點,而不是父節(jié)點当船。

PaintingContext.repaintCompositedChild

static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
  assert(child._needsPaint);
  _repaintCompositedChild(
    child,
    debugAlsoPaintedParent: debugAlsoPaintedParent,
  );
}
static void _repaintCompositedChild(
  RenderObject child, {
  bool debugAlsoPaintedParent = false,
  PaintingContext? childContext,
}) {
  assert(child.isRepaintBoundary);
  assert(() {
    // register the call for RepaintBoundary metrics
    child.debugRegisterRepaintBoundaryPaint(
      includedParent: debugAlsoPaintedParent,
      includedChild: true,
    );
    return true;
  }());
  OffsetLayer? childLayer = child._layer as OffsetLayer?;
  if (childLayer == null) {
    assert(debugAlsoPaintedParent);
    child._layer = childLayer = OffsetLayer(); //childLayer如果為空的話题画,就new一個OffsetLayer
  } else {
    assert(debugAlsoPaintedParent || childLayer.attached);
    childLayer.removeAllChildren();
  }
  assert(identical(childLayer, child._layer));
  assert(child._layer is OffsetLayer);
  assert(() {
    child._layer!.debugCreator = child.debugCreator ?? child.runtimeType;
    return true;
  }());
  childContext ??= PaintingContext(child._layer!, child.paintBounds);
  child._paintWithContext(childContext, Offset.zero);
  assert(identical(childLayer, child._layer));
  childContext.stopRecordingIfNeeded();
}

childLayer如果為空的話,就new一個OffsetLayer賦值給 child._layer = childLayer生年,然后把這個構(gòu)造PaintingContext傳給下面child繪制使用

void _paintWithContext(PaintingContext context, Offset offset) {
  if (_needsLayout)
    return;
  RenderObject? debugLastActivePaint;
  assert(() {
    _debugDoingThisPaint = true;
    debugLastActivePaint = _debugActivePaint;
    _debugActivePaint = this;
    assert(!isRepaintBoundary || _layer != null);
    return true;
  }());
  _needsPaint = false;
  try {
    paint(context, offset);
    assert(!_needsLayout); // check that the paint() method didn't mark us dirty again
    assert(!_needsPaint); // check that the paint() method didn't mark us dirty again
  } catch (e, stack) {
    _debugReportException('paint', e, stack);
  }
  assert(() {
    debugPaint(context, offset);
    _debugActivePaint = debugLastActivePaint;
    _debugDoingThisPaint = false;
    return true;
  }());
}

把PaintingContext傳給paint方法婴程,paint方法里面一般重繪完自身以后都會調(diào)用PaintingContext.paintChild方法去重繪子類,也會把這個PaintingContext一直傳下去抱婉,PaintingContext的ContainerLayerd的屬性就是OffsetLayer

PaintingContext.paintChild

void paintChild(RenderObject child, Offset offset) {
  assert(() {
    if (debugProfilePaintsEnabled)
      Timeline.startSync('${child.runtimeType}', arguments: timelineArgumentsIndicatingLandmarkEvent);
    debugOnProfilePaint?.call(child);
    return true;
  }());

  if (child.isRepaintBoundary) {
    stopRecordingIfNeeded();
    _compositeChild(child, offset);
  } else {
    child._paintWithContext(this, offset);      //非邊界節(jié)點档叔,直接傳遞this作為child的PaintingContext
  }

  assert(() {
    if (debugProfilePaintsEnabled)
      Timeline.finishSync();
    return true;
  }());
}

PaintingContext.paintChild方法里面也先調(diào)用stopRecordingIfNeeded桌粉,再調(diào)用_compositeChild,這里也就構(gòu)成了一個遞歸循環(huán)的調(diào)用一直到葉子節(jié)點的RenderObject的paint方法衙四,如果他不是邊界節(jié)點的話直接傳遞this作為child的PaintingContext铃肯,這里也就說明了如果沒有邊界節(jié)點的話那么所有的RenderObject都是在一個PaintingContext也就是一個ContainerLayer的圖層里面繪制的,如果有邊界截點的話那么就會重新申請新的ContainerLayer并且在新的圖層上面繪制

感覺內(nèi)容有點多传蹈,我借用網(wǎng)上的例子來給大家說一下押逼,下圖左邊是 widget 樹,右邊是最終生成的Layer樹惦界,我們看一下生成過程:

Pasted Graphic.png
  1. 一開始繪制會從RenderView開始挑格,因為他是一個繪制邊界節(jié)點(我們上面已經(jīng)講了什么是邊界節(jié)點),在第一次繪制時會為他創(chuàng)建一個 OffsetLayer沾歪,我們記為 OffsetLayer1漂彤,接下來 OffsetLayer1會傳遞給child
  2. 由于 Row 是一個容器類組件且不需要繪制自身,那么接下來他會繪制自己的孩子灾搏,它有兩個孩子挫望,先繪制第一個孩子Column1,將 OffsetLayer1 傳給 Column1狂窑,而 Column1 也不需要繪制自身媳板,那么它又會將 OffsetLayer1 傳遞給第一個子節(jié)點Text1。
  3. Text1 需要繪制文本泉哈,他會使用 OffsetLayer1進行繪制蛉幸,由于 OffsetLayer1 是第一次繪制,所以會新建一個PictureLayer1和一個 Canvas1 丛晦,然后將 Canvas1 和PictureLayer1 綁定巨缘,接下來文本內(nèi)容通過 Canvas1 對象繪制,Text1 繪制完成后采呐,Column1 又會將 OffsetLayer1 傳給 Text2 若锁。
  4. Text2 也需要使用 OffsetLayer1 繪制文本,但是此時 OffsetLayer1 已經(jīng)不是第一次繪制斧吐,所以會復(fù)用之前的 Canvas1 和 PictureLayer1又固,調(diào)用 Canvas1來繪制文本。
  5. Column1 的子節(jié)點繪制完成后煤率,PictureLayer1 上承載的是Text1 和 Text2 的繪制產(chǎn)物仰冠。
  6. 接下來 Row 完成了 Column1 的繪制后,開始繪制第二個子節(jié)點 RepaintBoundary蝶糯,Row 會將 OffsetLayer1 傳遞給 RepaintBoundary洋只,由于它是一個繪制邊界節(jié)點,且是第一次繪制,則會為它創(chuàng)建一個 OffsetLayer2识虚,接下來 RepaintBoundary 會將 OffsetLayer2 傳遞給Column2肢扯,和 Column1 不同的是,Column2 會使用 OffsetLayer2 去繪制 Text3 和 Text4担锤,繪制過程同Column1蔚晨,在此不再贅述。
  7. 當(dāng) RepaintBoundary 的子節(jié)點繪制完時肛循,要將 RepaintBoundary 的 layer( OffsetLayer2 )添加到父級Layer(OffsetLayer1)中铭腕。

整棵組件樹繪制完成,生成了一棵右圖所示的 Layer 樹多糠。需要說名的是 PictureLayer1 和 OffsetLayer2 是兄弟關(guān)系累舷,它們都是 OffsetLayer1 的孩子。通過上面的例子我們至少可以發(fā)現(xiàn)一點:同一個 Layer 是可以多個組件共享的夹孔,比如 Text1 和 Text2 共享 PictureLayer1笋粟,共享的話也會導(dǎo)致一個問題,比如 Text1 文本發(fā)生變化需要重繪會連帶著 Text2 也必須重繪析蝴,這個也是為什么有時候我們合理的使用RepaintBoundray可以提高重繪的效率了,就像上圖如果Text3的重繪只會影響到Column2而不會再影響到Row

為什么會有共享這種機制了绿淋,其實究其原因其實還是為了節(jié)省資源闷畸,Layer 太多時 Skia 會比較耗資源,所以這其實是一個trade-off吞滞。

上面提到了Canvas的使用佑菩,(一般來說在paint方法繪制自身的時候回使用到Canvas來進行繪制)所以讓我來看看代碼他是怎么使用的:

Canvas get canvas {
 //如果canvas為空,則是第一次獲炔迷殿漠;
 if (_canvas == null) _startRecording(); 
 return _canvas!;
}
//創(chuàng)建PictureLayer和canvas
void _startRecording() {
  _currentLayer = PictureLayer(estimatedBounds);
  _recorder = ui.PictureRecorder();
  _canvas = Canvas(_recorder!);
  //將pictureLayer添加到_containerLayer(是繪制邊界節(jié)點的Layer)中
  _containerLayer.append(_currentLayer!);
}

調(diào)用get屬性的時候會判斷為空則創(chuàng)建PictureLayer,以及ui.PictureRecorder(相應(yīng)的繪制產(chǎn)物PictureRecorder被保存在 PictureLayer.picture 對象中)佩捞,以及_canvas绞幌,將pictureLayer添加到_containerLayer(是繪制邊界節(jié)點的OffsetLayer)中

我們再來看上面遺留的stopRecordingIfNeeded,這個里面與_startRecording做了相反的操作一忱,只是把ui.PictureRecorder保存下來以后莲蜘,PictureLayer,ui.PictureRecorder帘营,_canvas都做了置空的操作票渠,這里面有個小細(xì)節(jié)有朋友可能會問為什么_currentLayer(PictureLayer)置空了,_currentLayer!.picture還能保留下來呢芬迄,這個是因為我們在_startRecording的時候把加入到了_containerLayer中了问顷,所以_containerLayer已經(jīng)保存了對象的引用,而_currentLayer只是另一個引用而已

void stopRecordingIfNeeded() {
  if (!_isRecording)
    return;
  assert(() {
    if (debugRepaintRainbowEnabled) {
      final Paint paint = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 6.0
        ..color = debugCurrentRepaintColor.toColor();
      canvas.drawRect(estimatedBounds.deflate(3.0), paint);
    }
    if (debugPaintLayerBordersEnabled) {
      final Paint paint = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1.0
        ..color = const Color(0xFFFF9800);
      canvas.drawRect(estimatedBounds, paint);
    }
    return true;
  }());
  _currentLayer!.picture = _recorder!.endRecording();
  _currentLayer = null;
  _recorder = null;
  _canvas = null;
}

好了,上面是我們從RenderView第一次繪制的時候大概流程杜窄,接下來我們再看看更新widget以后的重繪是什么樣子的呢:

首先我們還是回顧一下上一篇《Flutter運行原理篇之layout布局的過程》最后面提到的彩蛋(Widget更新會導(dǎo)致build肠骆,繼而導(dǎo)致layout):

void layout(Constraints constraints, { bool parentUsesSize = false }) {
  RenderObject? relayoutBoundary;
  // 先確定當(dāng)前組件的布局邊界
  if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    relayoutBoundary = this;
  } else {
    relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
  }
  // _needsLayout 表示當(dāng)前組件是否被標(biāo)記為需要布局
  // _constraints 是上次布局時父組件傳遞給當(dāng)前組件的約束
  // _relayoutBoundary 為上次布局時當(dāng)前組件的布局邊界
  // 所以,當(dāng)當(dāng)前組件沒有被標(biāo)記為需要重新布局羞芍,且父組件傳遞的約束沒有發(fā)生變化哗戈,
  // 且布局邊界也沒有發(fā)生變化時則不需要重新布局,直接返回即可荷科。
  if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
    return;
  }
  // 如果需要布局唯咬,緩存約束和布局邊界
  _constraints = constraints;
  _relayoutBoundary = relayoutBoundary;

  // 后面解釋
  if (sizedByParent) {
    performResize();
  }
  // 執(zhí)行布局
  performLayout();
  // 布局結(jié)束后將 _needsLayout 置為 false
  _needsLayout = false;
  // 將當(dāng)前組件標(biāo)記為需要重繪(因為布局發(fā)生變化后,需要重新繪制)
  markNeedsPaint();
}

Layout 最后對于子節(jié)點(不再是容器的RenderObject)最后都會調(diào)用markNeedsPaint方法畏浆,重這里開始我們就開始paint重繪的過程了胆胰,我們先看看這個函數(shù)

RenderObject.markNeedsPaint

void markNeedsPaint() {
  if (_needsPaint) return;
  _needsPaint = true;
  if (isRepaintBoundary) { // 如果是當(dāng)前節(jié)點是邊界節(jié)點
      owner!._nodesNeedingPaint.add(this); //將當(dāng)前節(jié)點添加到需要重新繪制的列表中。
      owner!.requestVisualUpdate(); // 請求新的frame刻获,該方法最終會調(diào)用scheduleFrame()
  } else if (parent is RenderObject) { // 若不是邊界節(jié)點且存在父節(jié)點
    final RenderObject parent = this.parent! as RenderObject;
    parent.markNeedsPaint(); // 遞歸調(diào)用父節(jié)點的markNeedsPaint
  } else {
    // 如果是根節(jié)點蜀涨,直接請求新的 frame 即可
    if (owner != null)
      owner!.requestVisualUpdate();
  }
}

下判斷是否是邊界節(jié)點,如果是的話直接加入到了_nodesNeedingPaint上面蝎毡,如果存在父節(jié)點又不是邊界的話直接調(diào)用parent.markNeedsPaint繼續(xù)向上遞歸查找

接著我再幫大家回顧一下整個Widget的更新運行要經(jīng)過5個步驟:

void drawFrame() {
  buildOwner!.buildScope(renderViewElement!); // 1.重新構(gòu)建widget
  super.drawFrame();
  //下面幾個是在super.drawFrame()執(zhí)行的
  pipelineOwner.flushLayout();          // 2.更新布局
  pipelineOwner.flushCompositingBits();     //3.更新“層合成”信息
  pipelineOwner.flushPaint();               // 4.重繪
  if (sendFramesToEngine) {
    renderView.compositeFrame();            // 5. 上屏厚柳,將繪制出的bit數(shù)據(jù)發(fā)送給GPU
  }
}

上面的build,layout我們都已經(jīng)介紹過了沐兵,忘記的朋友可以出門左轉(zhuǎn)即可别垮,接下來是合成的內(nèi)容,這個我們往下先放一放(因為合成的目的就是為了重繪)扎谎,接下來就到了重繪碳想,也就是我們今天的內(nèi)容

那我們就先來看看 pipelineOwner.flushPaint(); 函數(shù):

PipelineOwner.flushPaint()

void flushPaint() {
  if (!kReleaseMode) {
    Timeline.startSync('Paint', arguments: timelineArgumentsIndicatingLandmarkEvent);
  }
  assert(() {
    _debugDoingPaint = true;
    return true;
  }());
  try {
    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();
        }
      }
    }
    assert(_nodesNeedingPaint.isEmpty);
  } finally {
    assert(() {
      _debugDoingPaint = false;
      return true;
    }());
    if (!kReleaseMode) {
      Timeline.finishSync();
    }
  }
}

這里面會對于dirtyNodes(也就是我們上面添加的_nodesNeedingPaint)里面的每一個node都會執(zhí)行PaintingContext.repaintCompositedChild(node);方法,其實這個過程我們上面已經(jīng)分析過了毁靶,這部分內(nèi)容與第一次重繪的過程是一樣的:

PaintingContext.repaintCompositedChild

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

static void _repaintCompositedChild(
  RenderObject child, {
  bool debugAlsoPaintedParent = false,
  PaintingContext? childContext,
}) {
  assert(child.isRepaintBoundary);
  assert(() {
    // register the call for RepaintBoundary metrics
    child.debugRegisterRepaintBoundaryPaint(
      includedParent: debugAlsoPaintedParent,
      includedChild: true,
    );
    return true;
  }());
  OffsetLayer? childLayer = child._layer as OffsetLayer?;
  if (childLayer == null) {
    assert(debugAlsoPaintedParent);
    // Not using the `layer` setter because the setter asserts that we not
    // replace the layer for repaint boundaries. That assertion does not
    // apply here because this is exactly the place designed to create a
    // layer for repaint boundaries.
    child._layer = childLayer = OffsetLayer();
  } else {
    assert(debugAlsoPaintedParent || childLayer.attached);
    childLayer.removeAllChildren();
  }
  assert(identical(childLayer, child._layer));
  assert(child._layer is OffsetLayer);
  assert(() {
    child._layer!.debugCreator = child.debugCreator ?? child.runtimeType;
    return true;
  }());
  childContext ??= PaintingContext(child._layer!, child.paintBounds);
  child._paintWithContext(childContext, Offset.zero);

  // Double-check that the paint method did not replace the layer (the first
  // check is done in the [layer] setter itself).
  assert(identical(childLayer, child._layer));
  childContext.stopRecordingIfNeeded();
}

現(xiàn)在我們已經(jīng)說清楚了paint重繪的整個流程胧奔,如果你讀到這里的話那么你應(yīng)該清楚了從Widget更新開始經(jīng)過build,到layout再到paint的大致流程應(yīng)該清楚了预吆,給大家推一篇博客《繪制原理及Layer》我今天這篇文章部分內(nèi)容來自這篇博客龙填,說實話如果你有耐心的話我還是推薦你去看完他整篇內(nèi)容,好了拐叉,我們今天的內(nèi)容就到這里了觅够,新年到來之際還是祝大家新年快樂,升職加薪巷嚣,最重要的是身體健康喘先,天天開心,下期見···

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末廷粒,一起剝皮案震驚了整個濱河市窘拯,隨后出現(xiàn)的幾起案子红且,更是在濱河造成了極大的恐慌,老刑警劉巖涤姊,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暇番,死亡現(xiàn)場離奇詭異,居然都是意外死亡思喊,警方通過查閱死者的電腦和手機壁酬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恨课,“玉大人舆乔,你說我怎么就攤上這事〖凉” “怎么了希俩?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長纲辽。 經(jīng)常有香客問我颜武,道長,這世上最難降的妖魔是什么拖吼? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任鳞上,我火速辦了婚禮,結(jié)果婚禮上吊档,老公的妹妹穿的比我還像新娘篙议。我一直安慰自己,他們只是感情好籍铁,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著趾断,像睡著了一般拒名。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芋酌,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天增显,我揣著相機與錄音,去河邊找鬼脐帝。 笑死同云,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的堵腹。 我是一名探鬼主播炸站,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼疚顷!你這毒婦竟也來了旱易?” 一聲冷哼從身側(cè)響起禁偎,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎阀坏,沒想到半個月后如暖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡忌堂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年盒至,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片士修。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡枷遂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出李命,到底是詐尸還是另有隱情登淘,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布封字,位于F島的核電站黔州,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏阔籽。R本人自食惡果不足惜流妻,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望笆制。 院中可真熱鬧绅这,春花似錦、人聲如沸在辆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽匆篓。三九已至浑度,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鸦概,已是汗流浹背箩张。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留窗市,地道東北人先慷。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像咨察,于是被迫代替她去往敵國和親论熙。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容