Flutter 渲染管道原理

本文主要给大家分享flutter底层渲染管道的原理,主要涉及flutter的布局、绘制和图层合成的底层实现。

Flutter的整体架构图如下,最底层是引擎层,引擎层暴露了非常低级的API,引擎的一个很重要的作用是智能化的处理各种纹理排版,它知道如何拍摄矢量图形并把他们绘制在屏幕上。渲染层在框架图中是比较底层的一个层级,它负责组织各种组件(widget)并合理的对它们分配屏幕空间。


架构图

渲染管道

flutter完整的渲染管道涉及到很多步骤:

  1. 首先得到用户的输入,例如触摸屏幕事件,一些动画可能会随之产生,然后开始构建组件并去渲染它们;
  2. 渲染可以细分为3个子步骤;
    2.1. Layout(布局),它的作用是在屏幕上确定每个组件的大小和位置;
    2.2. Paint(绘制),它提供一系列方法把组件渲染成用户看到的样子;
    2.3. Composite(图层合成)它把绘制步骤生成的图层或者纹理堆叠在一起,按照顺序组织它们,以便它们可以高效的在屏幕上进行呈现,图层合成是组件最终呈现在屏幕上之前很关键的也是最后的一个优化步骤;
  3. 最后是光栅化,它把抽象的表现映射成物理像素显示在屏幕上。


    渲染管道

flutter渲染层设计遵循的原则:简单即高效

  1. 它只对渲染树遍历一次,实现了线性时间复杂度的布局算法和绘制算法;
  2. 它采用简单的盒约束模型进行布局,能够生成各种富有表现力的复杂布局方案;
  3. 它采用结构性的重绘和图层合成技术,充分利用了部分硬件能力。

flutter通过使用简单直接的算法和比较容易理解的属性,让渲染速度变得更快,使用了各种简单属性对算法进行了优化。它的布局和绘制使用了线性时间复杂度的算法,只遍历一次渲染对象树(RenderObject Tree)就知道应该把组件绘制成多大的样子。flutter的渲染方式与其他渲染系统有很大不同:一些渲染系统可能会进行多次布局,它们会从渲染对象树中的一个点出发去向下遍历从而收集计算出尽可能多的布局信息,然后再次向下遍历节点去调整组件的大小,一旦组件之间发生嵌套,就会产生O(n^2)的复杂度,因为它需要不断的向下遍历然后递归回去。flutter可以通过一次渲染树的遍历就计算出每个组件的布局,它使用非常简单的盒约束模型进行布局,通过约束求解器去确定组件最终的位置,flutter布局层面的盒模型与web浏览器中的那个盒模型不一样,flutter的盒约束模型基本就是一个具有最小宽度、最小高度、最大宽度和最大高度的盒子,它能生成各种富有表现力的复杂布局,用户也可以自定义约束求解器(BoxConstraint)去控制约束盒子的大小。flutter在渲染对象树中采用相同的绘制结构去决定哪棵子树需要重绘,而不是去追踪屏幕上哪些矩形区域绘制状态无效从而去重绘这些矩形,flutter的重绘在移动设备中充分利用了硬件的渲染加速能力。

1、Layout(布局)

渲染对象

flutter渲染树中的每个节点都是一个渲染对象RenderObject,它表示flutter中每个需要绘制的对象,RenderObject本身是一个非常抽象的概念,它有一个所有者属性,这个所有者是管理渲染管道的一个对象。每个RenderObject都知道它的父节点是谁,但一般来说RenderObject不知道关于子节点的信息,它只知道如何访问它的子节点,知道如何以抽象的方式进行布局和绘制它自己,这意味着不同的RenderObject可以自由拥有任何不同的子元素。parentData是RenderObject中用来存储子节点信息的数据结构(保存子节点位置等信息),它由父节点自己管理,它对子节点是透明无感知的,只有个别情况下是可以感知的,flutter中的RenderObject没有坐标系统的概念,它只知道它存在于渲染树中并且具有父节点。

布局数据流

布局数据流

flutter的布局过程采用深度遍历的方式对渲染对象树遍历一次,这个图描述了整个布局的数据流,从RenderObject的角度来看, 递归的盒约束不断向下传递,到达叶子节点之后依次返回每个节点的尺寸。(在RenderObject的角度看,父节点对子节点说你有多大尺寸,子节点说我得问问我的子节点,问完之后回复说我要某个具体的尺寸)

渲染盒

RenderObject有点抽象,有一个叫RenderBox的RenderObject会更加具体一些,RenderBox也是一类特殊的RenderObject,它继承自RenderObject,它拥有一个非常有用的坐标系笛卡尔坐标(x坐标和y坐标)、以及宽度和高度来表示该渲染对象的大小和位置,它还拥有一些固有的尺寸信息,可以计算出自己有多大。

盒约束

盒约束基本上就是图中描述的那样,每个渲染对象的宽度有最小值和最大值,并且高度也有最小值和最大值,约束规则是:如果是父节点给了子节点这些约束,那么子节点必须在这个约束区域内部的某个地方,不能太小也不能太大,使用这个简单的盒约束可以实际表达很多不同的布局,这意味着系统中的任何渲染对象必须为其父节点计算好自己的大小,举个例子:父节点对子节点说你只能是100*200这么大,因为只有这样你才能满足我的约束。

布局示例1

有一种布局范例称为宽度输入,高度输出。Web网页使用的就是这种布局方式,这种布局对文本排版非常有用,比如你需要展示一堆文字,你希望宽度恰好是200像素,但你不知道需要多高,你只需将宽度约束设置为 “紧”,并将高度约束设置为 “松” 即可,这样在布局过程中父节点向子节点指定宽度约束,子节点拿到数据之后去向父节点汇报它最终想要的高度即可。

布局示例2

很自然的想到与此相对的另一种布局,高度输入,宽度输出,和上述布局示例恰好相反。

节点位置存储

接下来我们看看父节点的位置数据如何存储的呢?我们注意到通过RenderBox每个渲染节点可以知道自己的大小,但还不知道自己的位置,因为元素的位置由父节点控制,意味着父节点获得所有子节点的大小后,父节点可以自由的去摆放这些子节点的位置而且不需要和这些子节点进行商量,BoxParentData就是RenderBox节点用来存储子节点位置信息的数据结构,所以它精简的只有一个offset属性。

布局示例

flex布局示例

我想通过一个例子介绍flex布局如何工作,flex布局是让你可以在一行Row或者一列Column中放置元素,为了简单起见,我们做一个行布局。 布局的时候上图中这4个组件对它们自己尺寸有多大产生了不同意见,有些组件指定了固定大小,有些组件比较灵活的想要瓜分剩余的空间,这些灵活的组件有不同的弹性系数。这个黄色方块的弹性系数为2,意味着它想占用两倍剩余空间,粉红色方块的弹性系数是1,意味着它想占用一倍剩余空间。布局算法的输入就是来自父组件的最小宽度、最大宽度、最小高度、最大高度约束(图中绿色部分所示),布局算法的输出就是每个组件的实际大小和位置,这里我们把最小宽高都设置为0,最大宽高是图中的绿色区域,我们完整的走一遍布局流程。

flex示例布局-步骤1

父节点首先询问子节点你想要多大空间,每个子节点都可以提出它们的意见,那么父节点应该给这些子节点哪些限制呢?根据父节点给的约束,子节点的高度可以是0,也可以和父节点传入的高度一样高。对于宽度,允许和子节点期望的一样小,但是父节点希望子节点的宽度能达到无限宽,为什么是无限宽?因为假如给了子节点最大宽度的限制,那么这时候每个子节点可能都会说我希望自己占满最大宽度,但是父节点可能没有那么大空间放得下这些子节点(子节点的个数大于1的时候),因此父节点给子节点最大宽度的约束条件对于实现flex行布局算法没啥好处,所以父节点干脆给子节点一个无穷大的宽度约束,无穷大在flutter的布局算法中表示父节点对子节点的大小没啥意见,子节点必须明确的告诉父节点自己需要占多大空间。因此对于示例的这个布局场景,蓝色的方块和红色的方块都会明确告诉父节点它们想要多大,然后父节点保存这些大小信息。

flex示例布局-步骤2

父节点把整体宽度减去这些非弹性布局节点(蓝色方块和红色方块)的宽度之和就算出了剩余的可支配空间,现在父节点有一些剩余空间,它需要将这些剩余空间分配给所有弹性布局的子节点,这样每个单位的弹性系数对应的宽度就是剩余空间 / ∑(弹性节点的弹性系数)

flex示例布局-步骤3

当我们布局弹性节点的时候给它们图中的这些约束,对于它们的宽度来说,父节点告诉它们约束(最小值和最大值一样),这个值足够它们填满父节点在布局中留下的所有剩余空间。 至于高度,取决于每个弹性节点,弹性节点可以随心所欲,也可以填满父节点输入的最大高度,子节点响应父节点布局请求的时候可以说它们同意这个高度,然后顺便告诉父节点它们希望的宽度是多少,这个布局正好遵循了我们刚刚介绍盒约束模型时候说的高度输入-宽度输出模型,现在父节点知道了每个子节点的大小,还需要计算它们的位置。

flex示例布局-步骤4

Flex行布局的定位算法非常简单。只需要按顺序定位每个节点即可,第一个节点放在空间中的第一个位置,然后第二个节点的位置是第一个的位置+第一个节点的宽度,以此类推……这样整体宽度就是输入的最大宽度,对于flex布局节点的高度来说,父节点可以选择把它们全部对齐到顶部、底部或者中心,所以父节点的高度是所有子节点高度的最大值。
这里需要注意的是,在父节点没有得出每个子节点大小的时候它是无法指定这些子节点的具体位置的,一旦知道了子节点的大小,父节点就一定能准确定位它们。
如果在flex布局的时候采用Width-In,Height-Out约束给每个弹性节点,父节点告诉子节点它们应该占据多宽,然后询问子节点得出具体高度就能实现垂直flex布局,上述举例采用Height-In,Width-Out生成了水平flex布局,flutter提供足够多的属性去控制布局算法生成各种各样的布局。刚开始的时候有人对flutter采用基于 RenderObject 的布局算法持怀疑态度,觉得做复杂的布局可能没法胜任,但事实证明,它的布局算法可以实现各种复杂布局。

重布局边界

重布局边界

为了让布局这件事变得更快,父节点有时候会给一些子节点严格约束,比如:让子节点具有固定的大小。这样做的好处是对布局算法的数据流做了切割,不管在重布局约束中的子节点发生任何变化导致flutter需要重新执行布局算法,都不会影响到渲染树中其余节点(其余节点不需要重新执行布局步骤)。因为每个节点只向布局算法汇报自己的大小,这就创造出了所谓的重布局边界,这样在flutter引擎需要绘制下一帧的时候只需要重新布局这个变化了的子树即可,所以说flutter采用的是线性布局算法,实际上该算法可以称为次线性算法,因为很多情况下重布局的时候渲染树中的很多节点都没有被重新访问。

严格约束只是能够在渲染树中产生重布局边界的一种情况,flutter还有很多其他情况能产生重布局边界,再举个例子:假如布局过程中子节点给父节点回复说我的尺寸取决于你给我传入的尺寸,不管你传入多大空间我都会把它填满,那么子节点的大小就完全可以由父节点确定,至于子节点的子节点有多大都无所谓,这样也可以形成一个重布局边界,很多使用场景下flutter约束求解器计算得出的增量布局其实挺小的。

绘制顺序

上文的案例可以知道布局算法访问每个节点的顺序,布局算法(案例以flex行布局为例)首先访问非弹性节点(蓝色和红色方块),然后访问了弹性节点(黄色和粉红色方块),绘制的时候按顺序从左到右依次绘制即可。但是在布局的时候却是按照不同的顺序访问的它们,flutter的这个布局算法和一些其他系统的做法形成了鲜明对比,有些系统布局和绘制过程中遍历渲染树中每个节点的顺序是一样的,因此这些系统最终不得不做一些特殊处理去适应绘制顺序和布局信息流顺序不一致的情况(复杂的布局可能需要)。Flutter的布局算法和绘制算法各自独立的去遍历渲染树,2次遍历的时间复杂度都是线性的。

这里引申一个问题:我们刚才分析的是flex行布局,假如遇到stack布局怎么办,其实flutter把stack这个widget对应的渲染对象移动到其他渲染节点的最顶部了,当然对于flutter的stack组件来说,它的子节点可能是已定位(positioned)的组件,也可能是未定位的其他组件,这样绘制的时候访问节点的顺序很关键,访问顺序直接决定最终用户看到的效果。

2、Paint(绘制)

下面说是flutter的绘制,绘制是如何工作的呢?flutter在布局阶段得到了每个组件的大小和位置,但是组件最终在屏幕上长啥样子还是不知道,那flutter如何绘制渲染对象树呢?

绘制过程数据流

看了这个图大家可能感觉绘制很简单,深度优先遍历一遍渲染对象树就行,父节点把位移传递给每个组件,然后让每个子节点在指定的位置绘制自己就行,因为到这个步骤的时候父节点已经知道了子节点在哪里以及子节点有多大,这时候唯一要做的就是绘制,其实远远没有这么简单。

图层结构

绘制真正的难点在于需要处理很多图层,假如我们有一个视频播放器组件,视频帧的内容可能来自于系统提供的不需要和用户进行交互的硬件,视频编解码器负责写入视频纹理,然后交给flutter进行绘制,但是我们想在视频画面底层绘制一些背景内容,在视频画面顶层绘制一些供用户交互的操作控件。这意味我们需要把视频播放器组件的绘制拆分成两个不同的部分,视频下方显示的部分和视频上方显示的部分,这样最终进行图层组合时才能形成我们期望的视觉效果。所以绘制最棘手问题是弄清楚先绘制哪个图层,然后再绘制哪个图层,flutter绘制阶段并没有产生显示在屏幕上的物理像素,从概念上讲,flutter只是采用图层作为屏幕像素绘制的缓冲区。

渲染节点绘制到图层

flutter在绘制阶段按照深度优先(前序遍历)的顺序遍历渲染树,然后把每个节点绘制到不同的图层中去,刚开始绘制指令处于蓝色图层,蓝色的节点被绘制到蓝色的图层,黄色的节点是视频帧或者一些需要被合成的东西,为了正确绘制它们,绘制指令被重定向到了独立的黄色图层,我们注意到绘制完第四个节点后,算法递归到第二个节点的时候发现它有一些绿色的部分和一些蓝色的部分,这是怎么出现的呢?想象一下假如绘制完了第二个节点的背景色,然后依次绘制完第三、第四个节点,然后算法递归到了第二个节点发现它想要绘制更多的内容,然后就出现了第五步的情况,这时候算法会把绘制指令从黄色图层重定向到绿色图层,然后算法递归到了第一个节点,第一个节点没有多余的绘制内容需要处理,然后到了第六个节点也没有多余的绘制要做,就直接把第六个节点的内容绘制到了绿色图层。这个绘制顺序意味着给定的一个渲染节点RenderObject,它的内容不一定被绘制到同一个图层中,而是可能被绘制到多个不同的图层中,flutter的绘制过程与很多其他系统不同,比如web浏览器,它是无法把单个渲染对象拆分到多个图层去绘制的。

绘制过程数据流

Flutter在深度向下遍历渲染树的时候设置子节点的偏移量,算法递归回去的时候子节点会告诉父节点需要绘制的目标图层是哪个,父节点告诉子节点具体的绘制位置,说你把自己绘制在这里吧,子节点绘制好自己之后,告诉父节点你可以切换到另一个图层进行绘制了(或者在当前图层继续绘制其他子节点)。flutter通过这种方式把每个节点需要绘制在哪个图层,以及绘制指令如何切换到不同图层的过程,通过一次渲染树的深度遍历就实现了。

重绘边界

节点重绘

我们看一个现象,假如上图中这个黄色的渲染节点可能会对绿色节点产生影响,就会使绘制过程复杂化,任何节点的变化都可能对渲染树中的其他节点产生影响,假如一个节点说我要被重绘,原则上系统重新把整棵渲染树重绘一遍就能实现视图刷新。

重绘边界示例

上文描述布局的时候陈述了flutter采用重布局边界进行布局优化的解决方案,flutter为了优化重绘的性能问题采用了重绘边界的解决方案,重绘边界的作用就是假如边界内的某个子节点需要重绘,那么该过程必须是局部的,不能因此让树中其他节点也跟着重绘。

重绘边界

引入重绘边界后,一些渲染节点被绘制的目标图层(layer)会发生变化,对于上图这棵渲染树来说,现在这个红色节点必须被绘制在红色图层中,哪怕下面这个黄色节点被删除了也不能改变红色节点被绘制的目标图层。重绘边界使算法更加稳定,绘制产生的影响只局限在个别子树中。

这里有个问题:flutter是否会通过绘制数据流自动计算出重绘边界?flutter怎么知道该把重绘边界放在渲染树的什么地方,并且重绘边界放在哪个位置才是最佳的方案?事实上重绘边界的放置比较灵活,假如给每个渲染节点都增加一个重绘边界,每个节点就都会使用单独的图层进行绘制,这样图层的数量就会大大增加,由于管理图层、纹理化图层都需要系统资源开销,把这些图层转换成GPU绘制的像素,还会额外产生很多无用的像素,因此这样反而会造成绘制效率低下的情况产生。但是我们又不希望重绘的过程中没有重绘边界(没有重绘边界就需要重绘整棵渲染树,这样绘制效率更低),所以最优的重绘边界设置方案应该介于没有重绘边界和给每个节点都增加重绘边界这2个方案之间,在什么地方增加重绘边界会对APP性能产生很大影响,所以重绘边界其实很难自动计算出最优值,因此flutter建议开发者考虑一下APP实现过程中的组件布局结构(合理利用StatelessWidget和StatefulWidget),多考虑一下当APP中某部分内容需要重绘时,哪些部分也会跟着被重绘?一个很好的例子是滚动组件:假设页面有2个组件,滚动组件在左侧,图片组件在右侧,滚动组件重绘的时候图片组件是不需要被重绘的,这时候我们当然希望有一个重绘边界介于这2个组件之间,下文我们分析滚动组件。

重绘边界

上图是为了展示如何进行节点重绘的,重绘边界的引入会改变图层树的结构。左边是原来的具有3个图层的图层树,flutter采用前序遍历的顺序进行绘制,右侧是加入了重绘边界之后的图层树结构,产生了新的黑色图层去包裹这个重绘边界产生的效应,同时会额外产生一个红色图层,它在黑色图层绘制完之后进行绘制,这里所谓的引入重绘边界导致图层树结构发生变化并不是手工引入的,flutter提供的很多组件(widget)可以自动的在渲染树中增加重绘边界(比如滚动组件)。当我们去实现非常复杂的布局,假如需要重新造一个自定义的滚动组件时,就需要考虑在哪里给它设置重绘边界这个问题了。

图层合成

滚动组件

绘制步骤会产生很多图层,拥有这些图层的好处就是通过它们可以非常快速地更新APP的视觉外观,只需要移动这些图层从而改变他们的偏移量就行, 因为我们已经把所有的渲染对象切成了很多图层,因此在移动图层从而改变APP视觉外观的时候不需要再次遍历渲染树了,我们以滚动组件为例:假设有一个可滚动的列表,绿色的部分是每个不同的item,其中蓝色的边框是滚动视窗(用户实际可以看到的部分),当我们向上滚动时,如果框架做的不够好的话会导致重绘整个视窗中的每个item(滚动的同时搜索渲染树,直到找到一个重绘的边界, 然后重绘子节点),滚动对系统来说是一项非常繁重的操作,flutter的做法是给每个item渲染节点采用单独的图层进行绘制,从而让这项操作尽量高效。

复合滚动

当我们从滚动容器的第一部分滚动到第二部分,flutter只是把这些item对应的图层移动了一下位置,并没有进行重布局操作,flutter会顺便缓存相关的绘制指令或者相应的像素。向上滚动时,flutter会为新出现的item创建新图层,然后绘制它,有了item新图层后随着滚动事件的触发,只需要移动这些item新图层即可。当这个绿色的 item 1 滚动出可视区域的时候,系统可以选择回收它的资源,资源回收清单对整个flutter系统都是可见的,之后可以把回收的这些图层移动到滚动列表的底部,这样在整个滚动过程中只需要创建有限数量的图层即可。这也是为什么flutter中其实每个item图层并不知道自己的偏移(offset),因为它们可能会被自由移动甚至被复用。这种图层复用机制可以使滚动过程中不需要处理很多工作,图层位移基本可以在1ms内完成。

3、问题

3.1 如何优化图层合成过程?
传统系统的合成操作通过纹理缓存屏幕像素,然后把纹理投射在屏幕上,flutter有时候也这样做,每个图层都是一个向量(vector),可以理解为一系列需要执行的指令集,可以把它们生成纹理,这样需要的时候就可以直接把这些像素投射在物理屏幕上了,但是问题难点在与什么时候去对这些图层生成纹理呢?
Flutter采用动态化的纹理方案(一个图层被重绘3次之后自动被缓存成纹理),假如一个图层被绘制了三次,那么flutter认为这个图层很有可能会被绘制第四次甚至更多次,这时候它就会给这个图层生成一个纹理,因此再次绘制它的时候只需要把纹理投射在屏幕即可。那flutter为何不选择给每个图层都生成一个纹理呢?举个例子:假设要实现一个转圈的进度条,转圈的时候其实任何2帧对应的像素都不是完全相同的,因此这种场景下给每个图层生成一个纹理是没有任何意义的,否则会导致系统内部产生大量纹理使纹理管理任务加重,但是假如每次都是直接绘制图层,永远不采用纹理的方式也无法使系统性能达到最优,举个例子:有一个抽屉列表,它每次显示和消失仅仅是在屏幕上显示的位移不同(内容完全没有变化),flutter在这种场景下会给这个抽屉组件对应的渲染树创建一个重绘边界,当这个抽屉完成3次重绘的时候,抽屉图层的纹理就会被flutter自动缓存,之后再次显示它的时候就仅仅操作纹理而已。

3.2 纹理的自动缓存为何用 3次 作为阈值?
3是个比较魔法的数字,其实就是一个经验值,就像市面上很多云存储系统都采用3备份策略一样,为何不是2备份、4备份呢?这个数字更多的是资源开销和所产生效益之间的一个平衡点而已。

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

推荐阅读更多精彩内容