Android 之你真的了解 View.post() 原理吗?

UI 优化系列专题,来聊一聊 Android 渲染相关知识,主要涉及 UI 渲染背景知识如何优化 UI 渲染两部分内容。


UI 优化系列专题
  • UI 渲染背景知识

View 绘制流程之 setContentView() 到底做了什么?
View 绘制流程之 DecorView 添加至窗口的过程
深入 Activity 三部曲(3)View 绘制流程
Android 之 LayoutInflater 全面解析
关于渲染,你需要了解什么?
Android 之 Choreographer 详细分析

  • 如何优化 UI 渲染

Android 之如何优化 UI 渲染(上)
Android 之如何优化 UI 渲染(下)


关于 View.post() 相信每个 Android 开发人员都不会感到陌生,它最常见的场景主要有两种。

  1. 更新 UI 操作

  2. 获取 View 的实际宽高

view.post() 的内部也是调用了 Handler,这可能是绝大多数开发人员所了解的,从本质来说这样理解并没有错,不过它并能解释上面提出的第 2 个场景。

在 Activity 中,View 绘制流程的开始时机是在 ActivityThread 的 handleResumeActivity 方法,在该方法首先完成 Activity 生命周期 onResume 方法回调,然后开始 View 绘制任务。也就是说 View 绘制流程要在 onResume 方法之后,但是我们绝大部分业务是在 onCreate 方法,比如要获取某个 View 的实际宽高,由于 View 的绘制任务还未开始,所以就无法正确获取。具体可以参考《View 绘制流程之 setContentView() 到底做了什么 ?

此时大家肯定使用过 View.post() 来解决该问题,注意 View 绘制流程也是向 Handler 添加任务,如果在 onCreate 方法直接使用 Handler.post(),则该任务一定在 View 绘制任务之前(同一个线程队列机制)。

  • 注意这里不考虑使用 ViewTreeObserver 或更长延迟的 postDelayed()。

那 View.post() 内部也是使用 Handler,它是如何实现的呢?简单来说,View.post() 对任务的运行时机做了调整。


View.post()

翻开 View 源码,找到 View 的 post 方法如下:

public boolean post(Runnable action) {
    // 首先判断AttachInfo是否为null
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        // 如果不为null,直接调用其内部Handler的post
        return attachInfo.mHandler.post(action);
    }

    // 否则加入当前View的等待队列
    getRunQueue().post(action);
    return true;
}

注意 AttachInfo 是 View 的静态内部类,每个 View 都会持有一个 AttachInfo,它默认为 null;需要先来看下 getRunQueue().post():

private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

getRunQueue() 返回的是 HandlerActionQueue,也就是调用了 HandlerActionQueue 的 post 方法:

public void post(Runnable action) {
    // 调用到postDelayed方法,这有点类似于Handler发送消息
    postDelayed(action, 0);
}

// 实际调用postDelayed
public void postDelayed(Runnable action, long delayMillis) {
    // HandlerAction表示要执行的任务
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        if (mActions == null) {
            // 创建一个保存HandlerAction的数组
            mActions = new HandlerAction[4];
        }
        // 表示要执行的任务HandlerAction 保存在 mActions 数组中
        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        // mActions数组下标位置累加1
        mCount++;
    }
}

HandlerAction 表示一个待执行的任务,内部持有要执行的 Runnable 和延迟时间;类声明如下:

private static class HandlerAction {
    // post的任务
    final Runnable action;
    // 延迟时间
    final long delay;

    public HandlerAction(Runnable action, long delay) {
        this.action = action;
        this.delay = delay;
    }

    // 比较是否是同一个任务
    // 用于匹配某个 Runnable 和对应的HandlerAction
    public boolean matches(Runnable otherAction) {
        return otherAction == null && action == null
                || action != null && action.equals(otherAction);
    }
}

注意 postDelayed() 创建一个默认长度为 4 的 HandlerAction 数组,用于保存 post() 添加的任务;跟踪到这,大家是否有这样的疑惑:View.post() 添加的任务没有被执行?

实际上,此时我们要回过头来,重新看下 AttachInfo 的创建过程,先看下它的构造方法:

AttachInfo(IWindowSession session, IWindow window, Display display,
               ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
               Context context) {
        mSession = session;
        mWindow = window;
        mWindowToken = window.asBinder();
        mDisplay = display;
        // 持有当前ViewRootImpl
        mViewRootImpl = viewRootImpl;
        // 当前渲染线程Handler
        mHandler = handler;
        mRootCallbacks = effectPlayer;
        // 为其创建一个ViewTreeObserver
        mTreeObserver = new ViewTreeObserver(context);
    }

注意 AttachInfo 中持有当前线程的 Handler。翻阅 View 源码,发现仅有两处对 mAttachInfor 赋值操作,一处是为其赋值,另一处是将其置为 null。

  • mAttachInfo 赋值过程:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // 给当前View赋值AttachInfo,此时所有的View共用同一个AttachInfo(同一个ViewRootImpl内)
    mAttachInfo = info;
    // View浮层,是在Android 4.3添加的
    if (mOverlay != null) {
        // 任何一个View都有一个ViewOverlay
        // ViewGroup的是ViewGroupOverlay
        // 它区别于直接在类似RelativeLaout/FrameLayout添加View,通过ViewOverlay添加的元素没有任何事件
        // 此时主要分发给这些View浮层
        mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
    }
    mWindowAttachCount++;

     // ... 省略

    if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER) != 0) {
        mAttachInfo.mScrollContainers.add(this);
        mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
    }
    //  mRunQueue,就是在前面的 getRunQueue().post()
    // 实际类型是 HandlerActionQueue,内部保存了当前View.post的任务
    if (mRunQueue != null) {
        // 执行使用View.post的任务
        // 注意这里是post到渲染线程的Handler中
        mRunQueue.executeActions(info.mHandler);
        // 保存延迟任务的队列被置为null,因为此时所有的View共用AttachInfo
        mRunQueue = null;
    }
    performCollectViewAttributes(mAttachInfo, visibility);
    // 回调View的onAttachedToWindow方法
    // 在Activity的onResume方法中调用,但是在View绘制流程之前
    onAttachedToWindow();

    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        for (OnAttachStateChangeListener listener : listeners) {
            // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
            // 但此时View还没有开始绘制,不能正确获取测量大小或View实际大小
            listener.onViewAttachedToWindow(this);
        }
    }

    // ...  省略

    // 回调View的onVisibilityChanged
    // 注意这时候View绘制流程还未真正开始
    onVisibilityChanged(this, visibility);

    // ... 省略
}

方法最开始为当前 View 赋值 AttachInfo。注意 mRunQueue 就是保存了 View.post() 任务的 HandlerActionQueue;此时调用它的 executeActions 方法如下:

public void executeActions(Handler handler) {
    synchronized (this) {
        // 任务队列
        final HandlerAction[] actions = mActions;
        // 遍历所有任务
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[i];
            //发送到Handler中,等待执行
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        //此时不在需要,后续的post,将被添加到AttachInfo中
        mActions = null;
        mCount = 0;
    }
}

遍历所有已保存的任务,发送到 Handler 中排队执行;将保存任务的 mActions 置为 null,因为后续 View.post() 直接添加到 AttachInfo 内部的 Handler 。所以不得不去跟踪 dispatchAttachedToWindow() 的调用时机。

ViewRootImpl

同一个 View Hierachy 树结构中所有 View 共用一个 AttachInfo,AttachInfo 的创建是在 ViewRootImpl 的构造方法中:

mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
            context);
  • 一般 Activity 包含多个 View 形成 View Hierachy 的树形结构,只有最顶层的 DecorView 才是对 WindowManagerService “可见的”。

dispatchAttachedToWindow() 的调用时机是在 View 绘制流程的开始阶段。在 ViewRootImpl 的 performTraversals 方法,在该方法将会依次完成 View 绘制流程的三大阶段:测量、布局和绘制,不过这部分不是今天要分析的重点。

// View 绘制流程开始在 ViewRootImpl
private void performTraversals() {
    // mView是DecorView
    final View host = mView;
    if (mFirst) {
        .....
        // host为DecorView
        // 调用DecorVIew 的 dispatchAttachedToWindow,并且把 mAttachInfo 给子view
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
        dispatchApplyInsets(host);
        .....
    } 
   mFirst=false
   ...
   // Execute enqueued actions on every traversal in case a detached view   enqueued an action
   getRunQueue().executeActions(mAttachInfo.mHandler);
   // View 绘制流程的测量阶段
   performMeasure();
   // View 绘制流程的布局阶段
   performLayout();
   // View 绘制流程的绘制阶段
   performDraw();
   ...

}

host 的实际类型是 DecorView,DecorView 继承自 FrameLayout。

  • 每个 Activity 都有一个关联的 Window 对象,用来描述应用程序窗口,每个窗口内部又包含一个 DecorView 对象,DecorView 对象用来描述窗口的视图 — xml 布局。通过 setContentView() 设置的 View 布局最终添加到 DecorView 的 content 容器中。

跟踪 DecorView 的 dispatchAttachedToWindow 方法的执行过程,DecorView 并没有重写该方法,而是在其父类 ViewGroup 中:

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

    // 子View的数量
    final int count = mChildrenCount;
    final View[] children = mChildren;
    // 遍历所有子View
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        // 遍历调用所有子View的dispatchAttachedToWindow
        // 为每个子View关联AttachInfo
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    // ...
}

for 循环遍历当前 ViewGroup 的所有 childView,为其关联 AttachInfo。子 View 的 dispatchAttachedToWindow 方法在前面我们已经分析过了:首先为当前 View 关联 AttachInfo,然后将之前 View.post() 保存的任务添加到 AttachInfo 内部的 Handler

注意回到 ViewRootImpl 的 performTraversals 方法,咋一看,这个过程好像没有太多新奇的地方。不过你是否注意到这一过程是在 View 的绘制任务中。

通过 View.post() 添加的任务,是在 View 绘制流程的开始阶段,将所有任务重新发送到消息队列的尾部,此时相关任务的执行已经在 View 绘制任务之后,即 View 绘制流程已经结束,此时便可以正确获取到 View 的宽高了

View.post() 添加的任务能够保证在所有 View(同一个 View Hierachy 内) 绘制流程结束之后才被执行

碎片化问题来了,如果我们只是创建一个 View,调用它的 post 方法,它会不会被执行呢?代码如下:

final ImageView view = new ImageView(this);
    view.post(new Runnable() {
        @Override
        public void run() {
            // do something
        }
    });

答案是否定的,因为它没有添加到窗口视图,不会走绘制流程,自然也就不会被执行。此时只需要添加如下代码即可:

// 将View添加到窗口
// 此时重新发起绘制流程,post任务会被执行
contentView.addView(view);

不过该问题在 API Level 24 之前不会发生,看下之前的代码实现:

// API Level 24之前的post实现
public boolean post(Runnable action) {
    // 这里的逻辑与API Level 24及以后一致
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // 主要是这里,此时管理待执行的任务直接交给了 ViewRootImpl 中。
    // 而在API Level 24及以后,每个View自行维护待执行任务队列,
    // 故,如果View不添加到Window视图,dispatchAttachedToWindow 不会被调用,
    // View中的post任务将永远得不到执行
    ViewRootImpl.getRunQueue().post(action);
    return true;
}

在 API Level 24 之前,通过 View.post() 任务被直接添加到 ViewRootImpl 中,在 24 及以后,每个 View 自行维护待执行的 post() 任务,它们要依赖于 dispatchAttachedToWindow 方法,如果 View 未添加到窗口视图,post() 添加的任务将永远得不到执行

这样的碎片化问题在 Android 中可能数不胜数,这也告诫我们如果对某项功能点了解的不够充分,最后可能导致程序未按照意愿执行。

至此,View.post() 的原理我们就算搞清楚了,不过还是有必要跟踪下 AttachInfo 的释放过程。

  • mAttachInfo 置 null 的过程:

先看下表示 DecorView 的 dispatchDetachedFromWindow 方法,实际是调用其父类 ViewGroup 中:

// ViewGroup 的 dispatchDetachedFromWindow
void dispatchDetachedFromWindow() {

    // ... 省略

    final int count = mChildrenCount;
    final View[] children = mChildren;
    // 遍历所有childView
    for (int i = 0; i < count; i++) {
        // 通知childView dispatchDetachedFromWindow
        children[i].dispatchDetachedFromWindow();
    }

    // ... 省略

    super.dispatchDetachedFromWindow();
}

不出所料 ViewGroup 的 dispatchDetachedFromWindow 方法会遍历所有 childView。

void dispatchDetachedFromWindow() {
    AttachInfo info = mAttachInfo;
    if (info != null) {
        int vis = info.mWindowVisibility;
        if (vis != GONE) {
            // 通知 Window显示状态发生变化
            onWindowVisibilityChanged(GONE);
            if (isShown()) {
                onVisibilityAggregated(false);
            }
        }
    }
    // 回调View的onDetachedFromWindow
    onDetachedFromWindow();
    onDetachedFromWindowInternal();

    // ... 省略

    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
        for (OnAttachStateChangeListener listener : listeners) {
            // 通知回调 onViewDetachedFromWindow
            listener.onViewDetachedFromWindow(this);
        }
    }

    // ... 省略

    // 将AttachInfo置为null
    mAttachInfo = null;
    if (mOverlay != null) {
        // 通知浮层View
        mOverlay.getOverlayView().dispatchDetachedFromWindow();
    }

    notifyEnterOrExitForAutoFillIfNeeded(false);
}

可以看到在 dispatchDetachedFromWindow 方法,首先回调 View 的 onDetachedFromWindow(),然后通知所有监听者 onViewDetachedFromWindow(),最后将 mAttachInfo 置为 null。

由于 dispatchAttachedToWindow 方法是在 ViewRootImpl 中完成,此时很容易想到它的释放过程肯定也在 ViewRootImpl,跟踪发现如下调用过程:

void doDie() {
    // 检查执行线程
    checkThread();

    synchronized (this) {
        if (mRemoved) {
            return;
        }
        mRemoved = true;
        if (mAdded) {
            // 回调View的dispatchDetachedFromWindow
            dispatchDetachedFromWindow();
        }

        if (mAdded && !mFirst) {
            destroyHardwareRenderer();

            // mView是DecorView
            if (mView != null) {
                int viewVisibility = mView.getVisibility();
                // 窗口状态是否发生变化
                boolean viewVisibilityChanged = mViewVisibility != viewVisibility;
                if (mWindowAttributesChanged || viewVisibilityChanged) {
                    try {
                        if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
                                & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
                            mWindowSession.finishDrawing(mWindow);
                        }
                    } catch (RemoteException e) {
                    }
                }
                // 释放画布
                mSurface.release();
            }
        }

        mAdded = false;
    }

    // 将其从WindowManagerGlobal中移除
    // 移除DecorView
    // 移除DecorView对应的ViewRootImpl
    // 移除DecorView
    WindowManagerGlobal.getInstance().doRemoveView(this);
}

可以看到 dispatchDetachedFromWindow 方法被调用,注意方法最后将 ViewRootImpl 从 WindowManager 中移除。

经过前面的分析我们已经知道 AttachInfo 的赋值操作是在 View 绘制任务的开始阶段,而它的调用者是 ActivityThread 的 handleResumeActivity 方法,即 Activity 生命周期 onResume 方法之后。

那它是在 Activity 的哪个生命周期阶段被释放的呢?在 Android 中, Window 是 View 的容器,而 WindowManager 则负责管理这些窗口,具体可以参考《View 绘制流程之 DecorView 添加至窗口的过程》。

我们直接找到管理应用进程窗口的 WindowManagerGlobal,查看 DecorView 的移除工作:

/**
 * 将DecorView从WindowManager中移除
 */
public void removeView(View view, boolean immediate) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }

    synchronized (mLock) {
        // 找到保存该DecorView的下标,true表示找不到要抛出异常
        int index = findViewLocked(view, true);
        // 找到对应的ViewRootImpl,内部的DecorView
        View curView = mRoots.get(index).getView();
        // 从WindowManager中移除该DecorView
        // immediate 表示是否立即移除
        removeViewLocked(index, immediate);
        if (curView == view) {
            // 判断要移除的与WindowManager中保存的是否为同一个
            return;
        }

        // 如果不是同一个View(DecorView),抛异常
        throw new IllegalStateException("Calling with view " + view
                + " but the ViewAncestor is attached to " + curView);
    }
}

根据要移除的 DecorView 找到在 WindowManager 中保存的 ViewRootImpl,真正移除是在 removeViewLocked 方法:

private void removeViewLocked(int index, boolean immediate) {
    // 找到对应的ViewRootImpl
    ViewRootImpl root = mRoots.get(index);
    // 该View是DecorView
    View view = root.getView();

    // ... 省略
    
    // 调用ViewRootImpl的die
    // 并且将当前ViewRootImpl在WindowManagerGlobal中移除
    boolean deferred = root.die(immediate);
    if (view != null) {
        // 断开DecorView与ViewRootImpl的关联
        view.assignParent(null);
        if (deferred) {
            // 返回 true 表示延迟移除,加入待死亡队列
            mDyingViews.add(view);
        }
    }
}

可以看到调用了 ViewRootImpl 的 die 方法,回到 ViewRootImpl 中:

boolean die(boolean immediate) {
    // immediate 表示立即执行
    // mIsInTraversal 表示是否正在执行绘制任务
    if (immediate && !mIsInTraversal) {
        // 内部调用了View的dispatchDetachedFromWindow
        doDie();
        // return false 表示已经执行完成
        return false;
    }

    if (!mIsDrawing) {
        // 释放硬件加速绘制
        destroyHardwareRenderer();
    } 
    // 如果正在执行遍历绘制任务,此时需要等待遍历任务完成
    // 故发送消息到尾部
    mHandler.sendEmptyMessage(MSG_DIE);
    return true;
}

注意 doDie 方法(源码在前面已经贴出),它最终会调用 dispatchDetachedFromWindow 方法。

最后,移除 Window 窗口任务是通过 ActivityThread 完成的,具体调用在 handleDestoryActivity 方法完成:

private void handleDestroyActivity(IBinder token, boolean finishing,
        int configChanges, boolean getNonConfigInstance) {
    // 回调 Activity 的 onDestory 方法
    ActivityClientRecord r = performDestroyActivity(token, finishing,
            configChanges, getNonConfigInstance);
    if (r != null) {
        cleanUpPendingRemoveWindows(r, finishing);

        // 获取当前Window的WindowManager, 实际是WindowManagerImpl
        WindowManager wm = r.activity.getWindowManager();
        // 当前Window的DecorView
        View v = r.activity.mDecor;
        if (v != null) {
            if (r.activity.mVisibleFromServer) {
                mNumVisibleActivities--;
            }
            IBinder wtoken = v.getWindowToken();
            // Window 是否添加过,到WindowManager
            if (r.activity.mWindowAdded) {
                if (r.mPreserveWindow) {
                    r.mPendingRemoveWindow = r.window;
                    r.mPendingRemoveWindowManager = wm;
                    r.window.clearContentView();
                } else {
                    // 通知 WindowManager,移除当前 Window窗口
                    wm.removeViewImmediate(v);
                }
            }
} 

注意 performDestoryActivity() 将完成 Activity 生命周期 onDestory 方法回调。然后调用 WindowManager 的 removeViewImmediate():

/**
 * WindowManagerImpl
 */
@Override
public void removeViewImmediate(View view) {
    // 调用WindowManagerGlobal的removeView方法
    mGlobal.removeView(view, true);
}

即 AttachInfo 的释放操作是在 Activity 生命周期 onDestory 方法之后,在整个 Activity 的生命周期内都可以正常使用 View.post() 任务。

总结
  1. 关于 View.post() 要注意在 API Level 24 前后的版本差异,不过该问题也不用过于担心,试想,会有哪些业务场景需要创建一个 View 却不把它添加到窗口视图呢?

  2. View.post() 任务能够保证在所有 View 绘制流程结束之后被调用,故如果需要依赖 View 绘制任务,此时可以优先考虑使用该机制。


最后,如果需要更好的理解 View.post() 执行原理,可能还需要进一步理解 AttachInfo 的创建过程,关于这部分的详细分析,你可以参考《Android 之 ViewTreeObserver 全面解析》。

文中如有不妥或有更好的分析结果,欢迎您的分享留言或指正。

文章如果对你有帮助,请留个赞吧!


扩展阅读

其他专题

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

推荐阅读更多精彩内容