我奶奶都能懂的UI绘制流程(下)!

1.前言

上回咱们说到ViewRootImpl.performTraversals()这个方法,从这里开始,会进入真正的View的绘制流程。第一次看的同学先去隔壁我奶奶都能懂的UI绘制流程(上)!汲取预备知识,剩下的同学系好安全带,发车啦!

2.Measure

2.1MeasureSpec

我们从performTraversals()开始参观,发现一上来就有个叫childWidthMeasureSpec的玩意儿

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);   
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

从名字上可以猜测,这是用来在测量时确定测量方式的。啥意思?在Android中,控件的大小有三种选择方式,match_parent,wrap_content以及具体的值。想一想,你叫系统wrap_content,它哪知道该怎么wrap_content,content有多大?父布局给它的空间又有多大?

所以这时候就需要MeasureSpec出场了。

在Measure流程中,系统将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,在onMeasure中根据这个MeasureSpec来确定view的测量宽高。

下面来具体看看这个类

 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
        ...

MeasureSpec是一个32位的int型数值,高2位表示mode,低30位表示size。

可以很清晰的看到,MeasureSpec有以下三种类型

1.EXACTLY :父容器已经测量出所需要的精确大小,这也是childview的最终大小
——match_parent,精确值
                
2.ATMOST : child view最终的大小不能超过父容器的给的大小
——wrap_content 
                
3.UNSPECIFIED: 不确定,源码内部使用
——一般在ScrollView,ListView 

但是开头的MODE_MASK = 0x3 << MODE_SHIFT又是什么鬼?

这就涉及到与或非操作了,这玩意儿不是给人看的,这句话没毛病,他们是给计算机看、以及程序员看的,程序员真的不是人。

从或("|")操作开始,在这里是用来将mode与size结合起来。

位运算或.png

接着看与("&"),MODE_MASK的作用就类似于网络中的掩码,是用来将内容过滤出来的,此处是用来获取mode。

位运算与.png

最后看与非("~&"),跟上面的与操作类似也是过滤内容,这里是用来将size过滤出来

位运算与非.png

到这里还是懵逼的道友,建议你们去学习下计算机组成原理相关的知识,在这里推荐下《程序是怎样跑起来的》(日)矢泽久雄著,感觉很棒。大家放心阅读,我没有淘宝链接。

2.2.getRootMeasureSpec

现在来看看int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width),我们需要注意第二个参数,找到他的起源:

 WindowManager.LayoutParams lp = mWindowAttributes;
final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            ...
        }

从上面的代码可以看到,lp会在初始化时调用父类的构造函数,其默认值是LayoutParams.MATCH_PARENT。现在回到getRootMeasureSpec(mWidth, lp.width)中,查阅这个方法完整的代码

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

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

可以看到,子View的MeasureSpec是由父View的LayoutParams决定的,这里一共有三种类型,验证了之前对MeasureSpec的总结。

而此时我们传进来的参数为LayoutParams.MATCH_PARENT,因此返回的childWidthMeasureSpec就是MeasureSpec.EXACTLY

2.3.View.measure()

准备好了参数,下面就来看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec),该方法会调用mView.measure(childWidthMeasureSpec, childHeightMeasureSpec),这个mView就是decorview,因此最终会调用view的measure()方法。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        ...
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        ...

        if (forceLayout || needsLayout) {
            ...
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

             ...
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

这个方法比较长,我们从上到下慢慢看。

首先看optical,这个就是光阴效果,在测量时,阴影也是要占空间的 。

接着看mMeasureCache,这是用来处理测量缓存的。结合后面的代码可以看到,测量前会先尝试着从mMeasureCache取出缓存,测量后又会将测量结果放置到缓存中。

最后onMeasure(widthMeasureSpec, heightMeasureSpec)是重点。在整个measure()方法中,我们并没有看到具体的测量代码,因为不同的View其测量方法也是不同的,需要由子类自己去决定。

这是一个典型的模板方法模式(设计模式之一,以后将Android架构的时候填坑),其中measure()是模板,由于所有控件最终都是继承自View的,因此只需要在View中实现measure()就可以了;而onMeasure()则需要由子View自定义,因此子View会重写onMeasure()方法。

2.4.View.onMeasure()

在阅读decorview的onMeasure()之前,我们先来看看View的onMeasure()方法

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

很简单,只是调用了setMeasuredDimension

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

可以看到这里也对光阴效果进行了处理,最后调用setMeasuredDimensionRaw()

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

在这里,我们对mMeasuredWidth以及mMeasuredHeight进行赋值。

有没有想起来什么?以前大家会在onCreate()方法中通过getMeasuredXXX()来获取控件的宽高,结果失败了,为什么?以getMeasuredHeight()为例

public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }

这里会返回mMeasuredHeight ,而mMeasuredHeight是在onResume()中通过ViewRootImpl进行一系列复杂的调用最终在View的setMeasuredDimensionRaw()中被赋值,所以在onCreate()中自然是获取不到的。

回到上面的方法中,在默认的情况下,这个measuredWidthmeasuredHeight又是哪来的呢?我们来看看getSuggestedMinimumWidth()做了什么

  protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

这个mMinWidth得记住它,在自定义控件时是很关键的一个数值。一般都需要为其赋值,可以通过代码与XML两种方式。

2.5.FrameLayout.onMeasure()

说了一大堆废话,现在我们回去看看DecorView的onMeasure()方法。

遗憾的是,这里面也没做具体的测量行为,反而是调用了super.onMeasure(widthMeasureSpec, heightMeasureSpec),也就是FrameLayout的onMeasure()

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                ...
                }
            }
        }
        ...
}

循环前获取了子View的数量,接着开始对每一个子View进行测量以获取其测量宽高。主要就是通过measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)方法。

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

是不是又有点熟悉?对啦,开始测量时,performTraversals()也是这样做的。

首先获取childWidthMeasureSpec以及childHeightMeasureSpec,然后通过child.measure(childWidthMeasureSpec, childHeightMeasureSpec)完成测量。很明显这是一个迭代的过程。

不同的是,performTraversals()获取到的是根节点的MeasureSpec,而这里要获取的是子View的MeasureSpec,因此要考虑的父View与本身两个因素.

2.5.1.MeasureSpec九兄弟

我们来看看getChildMeasureSpec(),一共有3*3=9种情况。文中我只介绍其中3种的代码

  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);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        ...
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

代码结构很清楚,首先判断父View的MeasureSpec,如果是MeasureSpec.EXACTLY,则开始判断子ViewchildDimension

1.如果是具体的>0点值,就直接将这个值赋给子View,并将类型设置为MeasureSpec.EXACTLY;
2.如果是LayoutParams.MATCH_PARENT,则将值设置为父容器的大小,类型为MeasureSpec.EXACTLY;
3.如果是LayoutParams.WRAP_CONTENT,则将值设置为父容器的大小,类型为 MeasureSpec.AT_MOST;

剩下的6种情况也是类似的,代码就不展示了,直接上总结的图片

getChildMeasureSpec方法分析.png

这张表不用刻意去记,先想一想,你会发现确实是这么一回事儿。

2.6 Measure总结

子View的测量在measureChildWithMargins()中也终于搞定,说了这么多,UI绘制的第一步measure终于差不多了,我们来总结下吧。
1、View的测量
onMeasure方法里面调用setMeasuredDimension()确定当前View的大小
2.、ViewGroup的测量
2.1、遍历测量Child,可以通过下面三个方法来遍历测量:ChildmeasureChildWithMarginsmeasureChildmeasureChildren
2.2、setMeasuredDimension 确定当前ViewGroup的大小

View树的源码measure流程图.png

3.Layout

前面花了那么大的篇幅介绍Measure过程,现在回过头再来看Layout就会比较简单,因为他们的套路都是一样的。

performLayout()开始,直接调用layout()方法,简洁明了

final View host = mView;
...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

其中host就是我们的decorview,来看看最关键的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);

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

      ...
    }

一开始,先要根据flag判断是否需要再次measure。

接着,将左上右下的位置依次赋值给oldL、oldT、oldR、oldB

继续,boolean changed的作用和measure中cache相似,是用来减少布局操作的。这儿是个三目运算符,根据有无光影调用不同的方法,我们以setFrame(l, t, r, b)为例

  protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            ...
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            // Invalidate our old position
            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            ...
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }
          ...
        }
        return changed;
    }

怎么样,是不是相当的通俗易懂?就是十分常见的缓存策略。

最后,layout()负责调用onLayout(changed, l, t, r, b),用志玲姐姐的话来说,“这是一个空箱子”

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

再去看看ViewGroup的onLayout(),更绝了,是个抽象方法。也就是说,每一个View的onLayout()都需要自己去实现。想想也是这个道理,自己想成为什么样的人,不是自己说了算吗?
在这里,为了观影体验,我们以FrameLayout为例,看看他的onLayout()做了什么

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

一上来就是为子View布局

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }
               ...
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

layoutChildren()本身也很简单明了,获取子控件数量,然后循环,依次获取宽高,判断各种情况,并调用子控件的布局方法。

没错就是这么简单,Layout也吹完了。

4.Draw

有了Measure和Layout的基础,Draw理解起来就更加简单了。
按照国际惯例,我们从ViewRootImpl.performDraw()看起

private void performDraw() {
 ...
draw(fullRedrawNeeded);
...

ViewRootImpl.draw()又会调用ViewRootImpl.drawSoftware(),然后调用mView.draw(canvas)。我们知道mView就是DecorView,而这个方法最终就会走进到View.draw()方法中。

public void draw(Canvas canvas) {
        ...
        /*
         * 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);
        }

这里有一个叫dirtyOpaque的标志。在自定义ViewGroup时,一般是不会调用onDraw方法的,除非设置了background。仔细想想这也是理所当然的,我没有背景,有什么好画的。这也是产生过度绘制的原因之一。

稍微拓展一下,为什么说LinearLayout比RelativeLayout绘制快?其实他们在measure和layout上所花的时间是差不多的,区别就在于draw,RelativeLayout要从左右、上下两个方向绘制,而LinearLayout只需要绘制一次。

第三步绘制内容也是同理,一般ViewGroup本身都不会有内容,有的只是childView。

 // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

最后看下绘制子View,这是个空方法,留给后人继承。

/**
     * Called by draw to draw the child views. This may be overridden
     * by derived classes to gain control just before its children are drawn
     * (but after its own view has been drawn).
     * @param canvas the canvas on which to draw the view
     */
    protected void dispatchDraw(Canvas canvas) {

    }

我们一般不会和他打招呼,draw更多的是应用在自定义View中,也就是说只要重写onDraw()方法即可。

到此为止,Draw也说完了,整个UI绘制结束!

5.实践是检验真理的唯一标准

说完原理,我们来看一个应用。

5.1 ScrollView与ListView不兼容问题

众所周知,在ScrollView中嵌套ListView时,ListView只会显示第一行,这是为什么呢?让我们一起走进ListView.onMeasure()的内心世界

if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

这是啥?在heightMode 为MeasureSpec.UNSPECIFIED时,ListView的高度竟然就只测量第一个childView的高度!

再来看看ScrollView,他重写了measureChildWithMargins(),有这么一句话

final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

搜滴寺内!ScrollView会将子View的HeightMeasureSpec设置为MeasureSpec.UNSPECIFIED,于是ListView到了这里就懵逼了。

至于解决方法的话,重写ListView或者ScrollView都可以,是不是感觉思路很清晰?

6.总结

真的连你奶奶都可以懂的!

完结撒花~

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

推荐阅读更多精彩内容