View 的绘制流程

View 的绘制流程是 Android 开发的必备知识点之一

API:26

View 树

因为这不是重点,所以介绍得比较简单,下次写文章详细讲解View树的形成与关系。

一个 Activity 会形成一棵以 ViewRoot 为根节点的 View 树

View树.png

ViewRoot 的实现类就是 ViewRootImlp,这棵树形成流程为:

  • Activity onCreate setContentView() 的时候解析后形成以 DecorView 为根节点的 View 树
  • Activity 启动的最后在 WindowManagerGlobal 生成 ViewRootImpl 并将之与DecorView绑定在一起

那问题来了,ViewRoot 不是 View 啊,怎么形成的树呢,实际上 ViewRootImpl 和 ViewGroup 都实现了 ViewParent 接口,View 有一个 mParent 字段存储父节点。


过程简介

View的绘制流程的起点是 DecorView 的 performTraversals() 方法开始的,顺着 View 树进行分发,而每一个View 经过 measure、layout、draw 后就被展示出来,其中,measure 是测量View的宽高,layout是确定在父View 中的位置,draw是将View绘制在屏幕上

View的绘制流程.png

从流程图上可知:

  • ViewRootImpl 的 performTraversals() 依次调用ViewRootImpl的 performMeasure() 、performLayout()、performDraw() 方法。
  • performMeasure() 又会调用子View的 measure(),measuer() 会调用自己的onMeasure() ,这时候onMeasure()又会对子View进行 measure
  • 同理,performLayout()、performDraw() 也是这样完成。

这就实现了 自顶向下 的测量、放置、绘制流程,而且要清楚 整个View树测量完成才开始放置,放置结束才开始绘制

MeasureSpec

在进行代码讲解之前,还需要了解 MeasureSpec。

MeasureSpec 是一个32位的 int 值。


  • SpecMode:测量模式,父View指定
  • SpecSize:测量大小,具体某个测量模式下View的大小

MeasureSpec 传递流程:
父 View会将自己的MeasureSpec传递下来,子 View 会根据父 View 的 MeasureSpec 和 View 本身的 LayoutParams 来确定自己的 MeasureSpec,从而进一步决定 View 的宽和高。

测量模式有三类:EXACTLY, UNSPECIFIED, AT_MOST。分别代表精确大小,不精确大小,最大值。

  • EXACTLY:父 View 不对 View 有任何限制,一般用于系统内部。
  • UNSPECIFIED:父View已知子View的大小,这个时候子View的最终大小就是MeasureSpec所指定的值,对应LayoutParams中的match_parent 和具体数值两种模式。
  • AT_MOST:父View指定可用大小(SpecSize),子 View 的大小不能大于这个值,对应LayoutParams中的warp_content。

有人说 MeasureSpec 是为了节约内存才这么做的,我觉得吧,系统也不缺一个int值,最主要是把 SpecMode、SpecSize 绑定在一起,一来可以不用在逻辑上保证两者的对应,减少代码复杂度,二来方便操作与传递。

performTraversals()

ViewRootImpl 是 ViewRoot的实现类,他的 performTraversals() 方法是 View 绘制流程的起点。

    private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;     // decorView
        // 省略若干...

            if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
                        updatedConfiguration) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // lp.width:MATCH_PARENT or WRAP_CONTENT, or an exact size
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // 就是window的大小

                     // 省略若干...
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 测量
                     // 省略若干...
                    if (measureAgain) {
                        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 递归的进测量
                    }
                }
            }
        } 
             // 省略若干...
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);  // 递归的进摆放
          // 省略若干...
            }
        }
        // 省略...
            performDraw();  // 递归的进行绘制
        } 
           // 省略...
    }

删减了部分代码后,我们能很清晰的看到 performTraversals() 依次调用了 ViewRootImpl

  • performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  • performLayout(lp, mWidth, mHeight);
  • performDraw();

进行测量、摆放、绘制的分发。

说明(个人理解):为什么要叫 childWidthMeasureSpec 呢?为什么不是widthMeasureSpec 呢?是因为这个值作用于子 View,对父 View 没啥用,但是是在父View中计算出来的

measure

ViewRootImpl

performTraversals() 通过 getRootMeasureSpec() 方法得到对应宽高的MeasureSpec, 然后将得到的宽高的 MeasureSpec 传递给 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec) 进行分发。

其中,传给 performMeasure 的 MeasureSpec 的大小就是window的大小,测量模式由Window的LayoutParams决定。代码如下:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // 指定了 Window 的大小,就以指定的为准
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

这个 MeasureSpec 是 ViewRootImpl 要传递给 DecorView 的,用于决定 DecorView 的大小。由此可知,DecorView默认情况下就是屏幕的大小

那传给 performMeasure 怎么处理的呢?

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

这里面比较简单,直接就分发给 mView了。 mView 是ViewRootImpl的成员变量,通过addView添加进来,就是DecorView。

相当于performMeasure() 直接交给了 View 的 measure()。

View

直接交给了View,那 View#Measure() 源码走起

    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        // 省略...
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
       // 省略...
        final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
        
        if (forceLayout || needsLayout) {  // 强制刷新或者需要测量的时候才会进行测量
            // 省略...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                onMeasure(widthMeasureSpec, heightMeasureSpec);  // 测量
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            // 省略...
        }
       // 省略...
    }

比较简单,无关的剔除了。代码很简单主要就是做记录、是否需要测量、调用onMeasure() 测量。

值得注意的是 measure() 方法是 final 的,所以ViewGroup 不可能覆写,我们也不能覆盖,所以我们如果要修改测量的逻辑,就只能在 onMeasure() 中重写。

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

默认实现比较简单粗暴,setMeasuredDimension 方法就是设置 View 宽高的测量值,调用 setMeasuredDimension 记录下来的值默认情况下就是View最后的大小。测量结束后一定要记得调用 setMeasuredDimension 方法保存测量值

那 getDefaultSize 方法怎么实现的呢?

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED: // 父 View 不对 View 有任何限制,一般用于系统内部
            result = size;
            break;
        case MeasureSpec.AT_MOST: // 对应 warp_content
        case MeasureSpec.EXACTLY:  // match_parent、具体数值
            result = specSize;
            break;
        }
        return result;
    }

对于AT_MOST、EXACTLY 两种测量模式下,就是直接返回 MeasureSpec 的测量大小。所以也是为什么自定义 View 的时候要自己重写 onMeasure 方法并设置 wrap_content 时候的 View 大小,因为默认情况下 wrap_content == match_parent 。同时我们也可以知道,正常app开发情况下,getSuggestedMinimumHeight()、getSuggestedMinimumWidth() 得到的值没用,我们选择忽略他。

所以,到这里基本知道了,如果分发给一个 View,如果没有重写 onMeasure 方法的话,他的父View传给他的 MeasureSpec 就可以决定他的测量结果。

那么问题来了,ViewRootImpl 分发给 DecorView 进行绘制,DecorView 是一个 ViewGroup,那他怎么实现ViewGroup 的测量呢?

viewGroup

ViewGroup 子类的布局特点会影响测量过程,ViewGroup 没有重写 onMeasure 方法,所以默认情况下,不测量子View,同时把自己当做一个View进行测量,比如 LinearLayout、RelativeLayout 等 ViewGroup 都需要重写 onMeasure 方法才能测量子 View并且特征化决定自己多大。

以 LinearLayout 垂直布局 wrap_content 为例,在决定自己有多大之前,会对每一个子View调用measure方法,并将初步高度累加存储在mTotalLength,而最终高度是 mTotalLength 与父View剩余高度取最小。

所以不同的 ViewGroup 子类决定了测量方式的不同,具体测量细节都被各种各样的 ViewGroup 所接管,与此同时,很多 ViewGroup 的子类会进行多次测量,这也是不同 ViewGroup 性能不同的原因之一。

但是,ViewGroup 提供了一些辅助方法帮助我们测量子 View ,而自己在默认情况下就是一个View的测量方式:ViewGroup 的父 View 说多大就多大。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

很简单,就是循环测量 Visibility 不为 GONE 的子 View,我们需要知道的是传入谁的 MeasureSpec 进来呢,需要传入的是当前 View 的 MeasureSpec。

那子View具体怎么测量的呢?

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width); //  lp.width:MATCH_PARENT or WRAP_CONTENT, or an exact size
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

获取到子View的 MeasureSpec 然后传递给子View 的 measure 方法,那是怎么得到子 View 的 MeasureSpec 呢?

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);  // 去除padding后父容器的剩余空间

        int resultSize = 0;  // 最终结果
        int resultMode = 0;

        switch (specMode) { // 父 View 的测量模式
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面方法主要是根据父View的 MeasureSpec 和子 View 的属性来最终决定 View 的大小,可以总结为以下规则:

childLayoutParams/parentSpecMode EXACTLY AT_MOST UNSPECIFIED
dp、px EXACTLY
childSize
EXACTLY
childSize
EXACTLY
childSize
match_parent EXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_content AT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0

测量总结

  1. 测量由 ViewRootImpl 的 performMeasure 方法分发下来。
  2. 决定子 View 的 MeasureSpec 是在父 View 中产生,由父 View 的测量模式、剩余空间和子 View 的 LayoutParams 共同决定
  3. 顶层View DecorView 的 默认大小就是窗口的大小
  4. ViewGroup 默认不测量子 View,测量细节自己决定,有可能需要测量多次,但是提供测量子View的辅助方法。

layout

Layout 过程相对于 measure 过程相对要简单一些,这个过程主要是确定元素的位置,layout 方法是 View 本身的位置,而onLayout 方法是确定子 View 的位置,这也是自定义ViewGroup 的时候必须要实现 onLayout 方法的原因。

ViewRootImpl

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;  // DecorView
        if (host == null) {
            return;
        }
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());  // 重点

            mInLayout = false;
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {  // 当View树在Layout过程的时候调用了 requestLayout() 就会进入这个分支
                  // 省略...
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }

代码结构很清晰,直接分发给了 DecorView 的 layout 方法。同时传递过去的大小就是测量后的结果。

ViewGroup、View

ViewGroup 重写了 View 的 layout 方法,我们先看下 ViewGroup 的 layout 方法。

    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

ViewGroup 重写的 layout 方法,实现很简单,就是在过渡的时候先不调用 layout 方法,结束再调用。正常情况下,还得看 View 的 layout 方法实现:

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);  // setFrame 设置四个顶点位置

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

View layout 方法主要就是调用了 setFrame(l, t, r, b) 方法设置 View 四个顶点的位置,确定了在父容器中的位置。然后调用 onLayout 方法确定子 View 的位置。

对于 View 来说,不需要确定子View的位置,所以默认是空实现:

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

而对于 ViewGroup 来说,onLayout 跟 onMeasure 方法有异曲同工之妙,两个都跟具体的布局有关,所以ViewGroup 整了一个abstract 方法,自定义 ViewGroup 必须实现:

    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

简单举下 LinearLayout 垂直布局的 onLayout 方法,其实就是遍历子 View ,并且在父 View 范围内,挨着挨着向下放置,放置过程很简单,距离顶部高度一直累加计算,最后计算出位置后,调用子 View 的 layout 方法。

draw

draw 过程具体实现已经控制比较复杂,但是我们可以知道 performDraw 方法向下分发,传递给 View 的 draw 方法中,draw 方法会调用 onDraw 方法进行绘制自己,那怎么向下分发?这里面是用 dispatchDraw 方法进行分发,而 draw 具体做了什么?

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas); // 绘制背景
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas); // 绘制自己

            // Step 4, draw the children
            dispatchDraw(canvas);  // 绘制子View

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas); // 绘制装饰(前景,滚动条)

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }
        // 省略...
        // 正常不走这里
    }

注释很清楚,主要是四大步(通常情况下另两步跳过):

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

推荐阅读更多精彩内容