Android UI刷新机制

1、概述

不论电脑,电视,手机,我们看到的画面都是由一帧帧的画面组成的。FPS是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。每秒钟帧数愈多,所显示的动作就会愈流畅。通常,要避免动作不流畅的最低是30。FPS”也可以理解为我们常说的“刷新率(单位为Hz)”。而在Android系统中每隔16.6ms会发送一次VSYNC信号有可能会触发UI的渲染。具体概念原理和流程在下面会详细讲解。

2、屏幕显示机制

在一个典型的显示系统中,一般包括CPU、GPU、display三个部分, CPU一般负责计算数据,把计算好数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer里存起来,然后display(有的文章也叫屏幕或者显示器)负责把buffer里的数据呈现到屏幕上。很多时候,我们可以把CPU、GPU放在一起说,那么就是包括2部分,CPU/GPU 和display。

2.1 绘制模型

① 软件绘制模型:

这里由CPU主导绘图,该模型先让视图结构(view hierarchy)失效,再绘制整个视图结构。当应用程序需要更新它的部分UI时,都会调用内容发生改变的View对象的invalidate()方法。无效(invalidation)消息请求会在View对象层次结构中传递,以便计算出需要重绘的屏幕区域(脏区)。然后,Android系统会在View层次结构中绘制所有的跟脏区相交的区域。这种绘制的缺点在于绘制了不需要重绘的视图。

② 硬件加速绘制模型

这里由GPU主导绘图,该模型先让视图结构失效,再记录和更新显示列表(Display List),这两步是在CPU当中完成的。最后在GUP中绘制显示列表。这种模式下,Android系统依然会使用invalidate()方法和draw()方法来请求屏幕更新和展现View对象。但Android系统并不是立即执行绘制命令,而是首先把这些View的绘制函数作为绘制指令记录一个显示列表中,然后再读取显示列表中的绘制指令调用OpenGL相关函数完成实际绘制。另一个优化是,Android系统只需要针对由invalidate()方法调用所标记的View对象的脏区进行记录和更新显示列表。没有失效的View对象就简单重用先前显示列表记录的绘制指令来进行简单的重绘工作。使用显示列表的目的是,把视图的各种绘制函数翻译成绘制指令保存起来,对于没有发生改变的视图把原先保存的操作指令重新读取出来重放一次就可以了,提高了视图的显示速度。而对于需要重绘的View,则更新显示列表,然后再调用OpenGL完成绘制。这种模型提高了Android系统显示和刷新的速度,但是它的兼容性没有软件模型好,同时更消耗内存也更加耗电。

2.2 VSYNC信号同步处理数据

在android 4.1以前UI不流畅问题较为严重,在4.1版本以后Android对显示系统进行了重构,引入了三个核心元素:VSYNC, Tripple Buffer和Choreographer。VSYNC是Vertical Synchronized的缩写,是一种定时中断;Tripple Buffer是显示数据的缓冲区;Choreographer起调度作用,将绘制工作统一到VSYNC的某个时间点上,使应用的绘制工作有序进行。

Android在绘制UI时,会采用一种称为“双缓冲”的技术,双缓冲即使用两个缓冲区(在SharedBufferStack中),其中一个称为Front Buffer,另外一个称为Back Buffer。UI总是先在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。

2.2.1 没有VSYNC同步信号

无同步信号

理想情况下,一个刷新会在16ms内完成(60FPS),也就是两个VSync信号之间。这样并不会出现问题,如上图第一个16ms开始,此时Display显示第0帧,CPU处理完第1帧后,GPU紧接其后处理继续第1帧。在第二个16ms,第1帧能正常显示在Display中。但在第二个16ms阶段CPU和GPU却并未及时去绘制第2帧数据(前面的空白区表示CPU和GPU忙其它的事),直到在本周期快结束时,CPU/GPU才去处理第2帧数据。这时当进入第三个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank),导致错过了显示第二帧。这种情况就会导致UI的卡顿感。

通过上述分析可知,此处发生Jank的关键问题在于,第1个16ms段内,CPU/GPU没有及时处理第2帧数据。 为解决这个问题,Android 4.1中引入了VSYNC,核心目的是解决刷新不同步的问题。

2.2.2 加入 VSYNC同步信号

有同步信号

在加入VSYNC信号同步后,每收到VSYNC中断,CPU就立刻开始处理各帧数据。这样就解决了刷新不同步的问题。

但是上图中仍然存在一个问题:如果在两个同步信号之间的这段时间里也就是说16ms中CPU和GPU处理绘制不完这就又会遇到卡顿的现象,如下图:

CPU/GPU处理超时

在第二个16ms时间段,Display本应显示B帧,但却因为GPU还在处理B帧,导致A帧被重复显示。同理,在第二个16ms时间段内,CPU无所事事,因为A Buffer被Display在使用。B Buffer被GPU在使用。同时,一旦过了VSYNC时间点,CPU就不能再被触发以处理绘制工作了。要等下一个VSYNC信号。由于只有两个Buffer(Android 4.1之前)GPU和CPU不能同时进行处理。为了解决这一问题,于是在Android4.1以后,引出了第三个缓冲区:Tripple Buffer。

2.2.3 加入Tripple Buffer

加入Tripple Buffer

第二个16ms时间段,CPU使用C Buffer绘图。虽然还是会多显示A帧一次,但后续显示就会流畅许多。虽然android4.1以后优化了绘制机制,但最好还是在16ms内能将展示界面绘制出来。

3、相关源码分析

Android中提供了View的invalidate()和postInvalidate()方法来让我们可以手动刷新界面。从这两个方法一步步向下查找最后都会调用ViewRootImpl的scheduleTraversals()。ViewRootImpl是一个非常重要的类,重点介绍一下。

3.1 ViewRootImpl

Android 设备呈现到界面上的大多数情况下都是一个 Activity,真正承载视图的是一个 Window,每个 Window 都有一个 DecorView,当调用 setContentView() 时其实是将自己写的布局文件添加到以 DecorView 为根布局的一个 ViewGroup 里,构成一颗 View 树。每个 Activity 对应一棵以 DecorView 为根布局的 View 树,DecorView 类里面有个 mParent属性,它的类型就是 ViewRootImpl,而且每个界面上的 View 的刷新,绘制,点击事件的分发其实都是由 ViewRootImpl 作为发起者的,由 ViewRootImpl 控制这些操作从 DecorView 开始遍历 View 树去分发处理。

ViewRootImpl和DecorView 的绑定时机是在执行完Android 的onResume()后,ActivityThread 会调用WindowManager的addView()方法。进而调用WindowManagerGlobal 的 addView()方法。


// WindowManagerGlobal .class

// 这时候传入的参数view 是DecorView

public void addView(View view, ViewGroup.LayoutParams params,

            Display display, Window parentWindow) {

    ...

        ViewRootImpl root;

        synchronized (mLock) {

            // 初始化ViewRootImpl

            root = new ViewRootImpl(view.getContext(), display);

    ...   

            // 存储DecorView和ViewRootImpl

            mViews.add(view);

            mRoots.add(root);

    ...

                // 绑定 DecorView和ViewRootImpl

                root.setView(view, wparams, panelParentView);

    ...

        }

    }

WindowManager的addView()等方法其实最后都是调用的WindowManagerGlobal 的相关方法,而WindowManagerGlobal 维护着所有 Activity 的 DecorView 和 ViewRootImpl都存储在mViews和mRoots中。接下来看一下 root.setView(view, wparams, panelParentView)


// ViewRootImpl.class

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

        synchronized (this) {

            if (mView == null) {

                mView = view;

    ...

                }

    ...

            // 发起布局请求

                requestLayout();

    ...

            // 将当前ViewRootImpl对象传入DecorView中

                view.assignParent(this);

    ...

            }

        }

    }

进入到view.assignParent(this):


// View.class

void assignParent(ViewParent parent) {

        if (mParent == null) {

            mParent = parent;

        } else if (parent == null) {

            mParent = null;

        } else {

            throw new RuntimeException("view " + this + " being added, but"

                    + " it already has a parent");

        }

    }

这里将ViewRootImpl赋值给DecorView的mParent 参数,从而将两者绑定起来。而子View 里面调用的invalidate()等方法最终通过while循环寻找parent 最后都会找到这个ViewRootImpl,再由它来进行操作。这个绑定操作是在执行完onResume()方法之后才被发起执行的,这也解释了在onCreate、onStart、onResume中不能获取View宽高的原因。ViewRootImpl 还未创建,就不存在渲染操作,也就不存在View的测量步骤了。换句话说一个 Activity 界面的绘制,其实是在 onResume() 之后才开始的。

回到ViewRootImpl的setView()方法,里面有这么一行代码:requestLayout(),进入查看其方法体:


// ViewRootImpl.class

@Override

    public void requestLayout() {

        if (!mHandlingLayoutInLayoutRequest) {

            checkThread();

            mLayoutRequested = true;

            scheduleTraversals();

        }

    }

同invalidate()和postInvalidate()这里也调用了scheduleTraversals()方法。这个方法是渲染屏幕的关键方法,由它来发起一次绘制View树的任务请求。

3.1.1 scheduleTraversals()


// ViewRootImpl.class

void scheduleTraversals() {

        if (!mTraversalScheduled) {

            mTraversalScheduled = true;

            // 发送同步屏障

            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

            mChoreographer.postCallback(

                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

        ...

        }

    }

mTraversalScheduled这个变量是为了过滤一帧内重复的刷新请求,初始值是false,在开始这一帧的绘制流程时候也会重新置为false(doTraversal()中,一会儿分析),同时,在取消遍历绘制 View 操作 unscheduleTraversals() 里也会设置为false。也就是说一般情况下在开始这一帧的正式绘制前,在这期间重复调用scheduleTraversals()只有一次会生效。这么设计的原因是前面已经说了和ViewRootImpl绑定的是DecorView,当刷新时候会对整个DecorView进行一次处理,所以不同view触发的scheduleTraversals()作用都是一样的,所以在这一帧里面只要有一次和多次刷新请求效果是一样的。

现在我们接着往下看


 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();

这行代码的意思是向主线程发送了一个同步屏障。同步屏障的作用可以理解成拦截同步消息的执行,主线程的 Looper 会一直循环调用 MessageQueue 的 next() 来取出队头的 Message 执行,当 Message 执行完后再去取下一个。当 next() 方法在取 Message 时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,如果有找到异步消息,那么就取出这个异步消息来执行,否则就让 next() 方法陷入阻塞状态。如果 next() 方法陷入阻塞状态,那么主线程此时就是处于阻塞状态的,也就是没在干任何事。所以,如果队头是一个同步屏障的消息的话,那么在它后面的所有同步消息就都被拦截住了,直到这个同步屏障消息被移除出队列,否则主线程就一直不会去处理同步屏幕后面的同步消息。而所有消息默认都是同步消息,只有手动设置了异步标志,这个消息才会是异步消息。另外,同步屏障消息只能由内部来发送,这个接口并没有公开给我们使用。

到这里也可以猜想出之后我们肯定会通过发送一个异步消息来进行UI渲染。而这么做的目的也可以理解,就是为了渲染操作能第一时间得到执行,而减少丢帧现象。同时也可以推理出在发送出这个同步屏障之前的其他同步消息还是能进行处理的。这里就可能出现同步屏障之前的消息处理过于耗时而导致的丢帧现象。

继续往下看下一行代码:


  mChoreographer.postCallback(

                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

这里调用了postCallback()函数,其中第一个函数是个标志函数,代表这个回调是处理布局绘制的。我们来看下第二个参数,它是个 Runnable 对象:


final class TraversalRunnable implements Runnable {

        @Override

        public void run() {

            doTraversal();

        }

    }

这个Runnable 做的事情就是调用了doTraversal()方法:

// ViewRootImpl.class

void doTraversal() {

        if (mTraversalScheduled) {

            mTraversalScheduled = false;

            // 移除同步屏障

            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            ...

            performTraversals();

            ...

        }

    }

在doTraversal()方法中的前几行代码正好和scheduleTraversals()中的前几行相反,它首先将mTraversalScheduled设置为false,同时移除同步屏障。可以猜测从这边开始要进行绘制操作了,我们也确实在这个方法里面看到了执行 performTraversals()这个函数:

// ViewRootImpl.class

private void performTraversals() {

    ...

    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

    ...

    performLayout(lp,mWidth,mHeight);

    ...

    performDraw();

    ...

}

performTraversals()的方法体很长,但其中最关键的三行代码如上所示,分别代表对DecorView 的测量、布局、绘制三大流程,而DecorView 依次递归遍历其子view也进行performTraversals()的调用。里面包含着一些逻辑判断,来确定这三大流程是否都要执行。有时可能只需要执行 performDraw() 绘制流程,有时可能只执行 performMeasure() 测量和 performLayout() 布局流程(一般测量和布局流程是一起执行的)。

我们回到先前的


 mChoreographer.postCallback(

                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

第三个参数是传一个token,这边传null就代表没有传token。

3.2 Choreographer

接下来分析下这个postCallback到底做了什么,以及这个函数的调用者mChoreographer。

 // Choreographer.class

public void postCallback(int callbackType, Runnable action, Object token) {

        postCallbackDelayed(callbackType, action, token, 0);

    }

// delayMillis传的参数是0

public void postCallbackDelayed(int callbackType,

            Runnable action, Object token, long delayMillis) {

        ```

        postCallbackDelayedInternal(callbackType, action, token, delayMillis);

    }

private void postCallbackDelayedInternal(int callbackType,

            Object action, Object token, long delayMillis) {

        ···

        synchronized (mLock) {

            final long now = SystemClock.uptimeMillis();

            final long dueTime = now + delayMillis;

            // action是前面传入的TraversalRunnable 回调函数

            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {

            // delayMillis传入的是0,进入这里

                scheduleFrameLocked(now);

            } else {

                ···

            }

        }

    }

postCallback()一路下来走到了postCallbackDelayedInternal()中,并且由于传了 delay = 0 进去,所以在 postCallbackDelayedInternal() 里面会先根据当前时间戳将这个 Runnable 保存到一个 mCallbackQueue 队列里,这个队列跟 MessageQueue 很相似,里面待执行的任务都是根据一个时间戳来排序。然后走到了 scheduleFrameLocked() 方法这边:

 // Choreographer.class

private void scheduleFrameLocked(long now) {

        if (!mFrameScheduled) {

            mFrameScheduled = true;

            if (USE_VSYNC) { // 默认为true

                ···

                //如果在Looper主线程上运行,则立即安排vsync,

                 //否则的话发送一个Message ,尽快从UI线程安排vsync。

                if (isRunningOnLooperThreadLocked()) {

                    scheduleVsyncLocked();

                } else {

                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);

                    msg.setAsynchronous(true);

                    mHandler.sendMessageAtFrontOfQueue(msg);

                }

            } ...

        }

    }

这里面进去后如果在Looper线程(即mHandler绑定的这个线程)上运行,则立即安排vsync,否则在消息队列头插入这一消息,以期望mHandler来得到尽快处理。

看一下scheduleVsyncLocked()和这个mHandler怎么处理这个消息:


private final class FrameHandler extends Handler {

        ...

        @Override

        public void handleMessage(Message msg) {

            switch (msg.what) {

                ...

                case MSG_DO_SCHEDULE_VSYNC:

                    doScheduleVsync();

                    break;

            ...

            }

        }

    }

void doScheduleVsync() {

        synchronized (mLock) {

            if (mFrameScheduled) {

                scheduleVsyncLocked();

            }

        }

    }

private void scheduleVsyncLocked() {

        mDisplayEventReceiver.scheduleVsync();

    }

可见mHandler处理的消息最后也会调用到scheduleVsyncLocked(),然后调用


mDisplayEventReceiver.scheduleVsync();

// FrameDisplayEventReceiver.class

public void scheduleVsync() {

        if (mReceiverPtr == 0) {

            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "

                    + "receiver has already been disposed.");

        } else {

            nativeScheduleVsync(mReceiverPtr);

        }

    }

最后会调用到native方法nativeScheduleVsync(mReceiverPtr)中去。这个方法其实相当于向系统订阅一个接受一个Vsync信号。android系统每过16.6ms会发送一个Vsync信号。但这个信号并不是所有app都能收到的,只有订阅了的才能收到。这样设计的合理之处在于,当UI没有变化的时候就不会去调用nativeScheduleVsync(mReceiverPtr)去订阅,从而也就会收到Vsync信号,也就不会有不必要的绘制操作。同时每次订阅只能收到一次Vsync信号,如果需要收到下次信号需要重新订阅。

3.3 FrameDisplayEventReceiver

既然是mDisplayEventReceiver用scheduleVsync()来订阅Vsync信号的,也是由这个类来接收Vsync信号的。

我们来看下这个类:

// Choreographer.class

private final class FrameDisplayEventReceiver extends DisplayEventReceiver

            implements Runnable {

        ...

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {

            super(looper, vsyncSource);

        }

        @Override

        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

        ...

            mTimestampNanos = timestampNanos;

            mFrame = frame;

            Message msg = Message.obtain(mHandler, this);

            msg.setAsynchronous(true);

            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);

        }

        @Override

        public void run() {

            mHavePendingVsync = false;

            doFrame(mTimestampNanos, mFrame);

        }

    }

上面这个类中的onVsync()函数就是来回调Vsync信号,这个回调方法里面将这个类自己的run()方法传给mHandler来进行处理,同时将这个消息设为异步消息。这和我们之前设置异步屏障来优先处理异步消息对上了。我们继续看起run方法中执行的函数 doFrame(mTimestampNanos, mFrame);

// Choreographer.class

void doFrame(long frameTimeNanos, int frame) {

        final long startNanos;

        synchronized (mLock) {

        ...

        try {

        ...

        // Choreographer.CALLBACK_TRAVERSAL这个参数和 mChoreographer.postCallback()里面传入的一致

            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

        ...

        }

        ...

    }

void doCallbacks(int callbackType, long frameTimeNanos) {

        CallbackRecord callbacks;

        synchronized (mLock) {

            ...

            // 取出之前放入mCallbackQueues的mTraversalRunnable

            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(

                    now / TimeUtils.NANOS_PER_MS);

            ...

            // 回调mTraversalRunnable的run函数

            for (CallbackRecord c = callbacks; c != null; c = c.next) {

                c.run(frameTimeNanos);

            }

            ...

        }

    }

到这里基本上已经走通整个流程了,doFrame()里面的doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);函数会回调之前 mChoreographer.postCallback()里面传入的mTraversalRunnable,进而最终调用performTraversals()来计算下一阵的数据,等到下一帧将计算的数据显示到屏幕上。

4、总结

整个UI渲染流程可以归纳为如下过程:

① 界面上任何一个 View 的刷新请求最终都会走到 ViewRootImpl 中的 scheduleTraversals() 里来安排一次遍历绘制 View 树的任务。

② scheduleTraversals() 会先过滤掉同一帧内的重复调用,确保同一帧内只需要安排一次遍历绘制 View 树的任务,遍历过程中会将所有需要刷新的 View 进行重绘。

③ scheduleTraversals() 会往主线程的消息队列中发送一个同步屏障,拦截这个时刻之后所有的同步消息的执行,但不会拦截异步消息,以此来尽可能的保证当接收到屏幕刷新信号时可以尽可能第一时间处理遍历绘制 View 树的工作。

④ 发完同步屏障后 scheduleTraversals() 将 performTraversals() 封装到 Runnable 里面,然后调用 Choreographer 的 postCallback() 方法。

⑤ postCallback() 方法会先将这个 Runnable 任务以当前时间戳放进一个待执行的队列里,然后如果当前是在主线程就会直接调用一个native 层方法,如果不是在主线程,会发一个最高优先级的 message 到主线程,让主线程第一时间调用这个 native 层的方法。

⑥ native 层的这个方法是用来向底层订阅下一个屏幕刷新信号Vsync,当下一个屏幕刷新信号发出时,底层就会回调 Choreographer 的onVsync() 方法来通知上层 app。

⑦ onVsync() 方法被回调时,会往主线程的消息队列中发送一个执行 doFrame() 方法的异步消息。

⑧ doFrame() 方法会去取出之前放进待执行队列里的任务来执行,取出来的这个任务实际上是 ViewRootImpl 的 doTraversal() 操作。

⑨ doTraversal()中首先移除同步屏障,再会调用performTraversals() 方法根据当前状态判断是否需要执行performMeasure() 测量、perfromLayout() 布局、performDraw() 绘制流程,在这几个流程中都会去遍历 View 树来刷新需要更新的View。

⑩ 等到下一个Vsync信号到达,将上面计算好的数据渲染到屏幕上,同时如果有必要开始下一帧的数据处理。

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

推荐阅读更多精彩内容