Android中View绘制流程源码分析

1. View绘制流程的概览。

  • 1.1 绘制流程的开始是从ViewRootImpl类的performTraversals() 开始的,看看代码
    private void performTraversals() {
      
      int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
      int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
      //measure
      mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      //layout
      mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
      //draw
      mView.draw(canvas);
      
    }

这里可以看到依次执行了View的measure(),layout(),draw()。这就是绘制一个View的流程。
那么依次看一下View这3个方法的源码

  • 1.2 measure():
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
           
            onMeasure(widthMeasureSpec, heightMeasureSpec);
      
          if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }
    }

可以看到,measure()是一个final方法,是我们无法操作的,所以onMeasure()要重点关注的,并且onMeasure()里必须调用setMeasuredDimension(),不然之后就会抛出异常,那么看一下onMeasure()的源码:

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

可以看到,这里只是单纯的调用了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:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }


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

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

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

可以看到,在getDefaultSize()中通过MeasureSpec.getMode()获取了specMode,通过MeasureSpec.getSize()获取size,这里可以借鉴使用

然后在setMeasuredDimension()中调用了setMeasuredDimensionRaw(),然后在这个方法里最终完成对mMeasuredWidth,mMeasuredHeight的赋值,以及对mPrivateFlags的更新。所以测量的宽高是通过setMeasuredDimension()保存的,同时setMeasuredDimension()后测量的宽高就有值了,可以通过getMeasuredWidth()获取测量的宽,通过getMeasuredHeight()获得测量的高。

小结:measure()是一个final方法,它的方法体里调用了onMeasure(),一般自定义View时可以通过onMeasure()对宽高进行修正,然后在onMeasure()里调用setMeasuredDimension()对测量结果进行保存,所以重写onMeasure()必须调用setMeasuredDimension(),否则无法保存测量结果,并会抛出异常。同时onMeasure()执行完后才能通过getMeasuredWidth()和getMeasuredHeight()获取测量的宽高,因为setMeasuredDimension()是在onMeasure()里执行的

可以通过MeasureSpec.getMode()获取模式,通过MeasureSpec.getSize()获取宽高

  • 1.3 layout():
    public void layout(int l, int t, int r, int b) {
    //根据传入的值判断布局是否改变过,并调用setFrame()
        boolean changed = setFrame(l, t, r, b);
    //如果布局改变过调用onLayout()
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
        }

     
    }

可以看到,先是通过setFrame(),判断布局是否改变过,如果改变过就调用onLayout(),那么先看一下它们的代码:

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;
    //记录改变后的左上右下的值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
        return changed;
    }
    
    //空实现
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

可以看到如果左右上下任何一个值发生了改变,那么代表布局发生了改变,改变后对新的左右上下的值进行了记录

然后onLayout是一个空实现。

小结:layout()接收了父View传过来的左上右下四个值,然后在setFrame()里根据这个值是否改变判断了是否更新布局,并且记录了自身最新的左上右下值,如果发生改变那么会调用onLayout()重新布局,需要调用着自己去实现

  • 1.4 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
          //如果需要的话绘制背景
          if (!dirtyOpaque) {
              drawBackground(canvas);
             }
      
        // Step 2, save the canvas' layers
        //保存绘制图层
         if (drawTop) {
             canvas.saveLayer(left, top, right, top + length, null, flags);
         }

        // Step 3, draw the content
        //绘制内容
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        //绘制子view
        dispatchDraw(canvas);
      
      // Step 5, draw the fade effect and restore layers
      //绘过渡效果和恢复涂层
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }
      
       // Step 6, draw decorations (scrollbars)
             //绘制滚动条
             onDrawScrollBars(canvas);

    }

可以看到,在源码中官方给出了6个步骤,但是只需要关注步骤3和步骤4就行了,那么分别看一下它们的源码:

    protected void onDraw(Canvas canvas) {
    
    }
    
    protected void dispatchDraw(Canvas canvas) {

    }

都是空实现,onDraw()是用来绘制自己的,dispatchDraw()是用来分发绘制子view的。

小结:draw()中对背景等内容进行了绘制,同时通过onDraw()绘制自身,通过dispatchDraw()分发绘制子View

2. VIew绘制流程细节分析

前面分析了View的大概流程,现在看一下一些细节

  • 2.1 先看measure(), 前面分析过,measure()是一个final方法,那么只能通过onMeasure去完成测量

测量的时候有一个很重要的概念MeasureSpec,它封装了specMode和specSize,specMode是父View对子VIiew的要求,它有3种模式:
MeasureSpec.EXACTLY:确定模式,父View期望子View的大小是一个已确定的值,也就是这个specSize
MeasureSpec.AT_MOST:至多模式,父View期望子View最大是某个值
MeasureSpec.UNSPECIFIED:未限定模式,父View对子View不做限制,如scrollView

了解了测量模式那么再看一下自定义View时的测量,之前了解到测量是调用measure(),然后可以在onMeasure()中进行修改,现在有2种情况,1是自定义view对自身进行修改,2是测量子view,分别看一下:

2.1.1 通过代码先看修改自身:

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //模拟自定义View自定义的宽高
        int realWidthSize=100;
        int realHeightSize=100;
        //获取mode和size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
                //根据父View对子View的期望mode
        switch (widthMode) {
            //如果父view希望子view是一个确定的值,那么realWidthSize的修改无效,只能是widthSize
            case MeasureSpec.EXACTLY:
                realWidthSize = widthSize;
                break;
            //如果父view希望子view最多是一个值,那么当realWidth最大只能是widthSize
            case MeasureSpec.AT_MOST:
                if (realWidthSize > widthSize) {
                    realWidthSize = widthSize;
                }
                break;
            //如果是未限定模式,那么realWidthSize不变,想多大就多大
            case MeasureSpec.UNSPECIFIED:
                realWidthSize = realWidthSize;
                break;
        }
                //修正完成后通过setMeasuredDimension()保存结果,这里realHeightSize的修正是一样的
        setMeasuredDimension(realWidthSize,realHeightSize);

    }

以上就是对自定义view后对宽高的修正过程,可以看出其实都是模版代码,其实Android已经提供了方法进行修正,看下代码:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //模拟自定义View
        int realWidthSize=100;
        int realHeightSize=100;
                //通过resolveSize()对结果进行修正
        int resolveWidthSize = resolveSize(realWidthSize, widthMeasureSpec);
        int resolveHeightSize = resolveSize(realHeightSize, heightMeasureSpec);
        //保存修正后的结果
        setMeasuredDimension(resolveWidthSize,resolveHeithSize);

    }

2.1.2 再看测量子view:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                //获取子view
        View child = getChildAt(0);
        //获取子view的layoutParams,获取childWidth,然后定义mode
        LayoutParams layoutParams = child.getLayoutParams();
        int childWidth = layoutParams.width;
        int childMode=0;
                //获取mode和size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        switch (widthMode) {
            //如果父view的要求是EXACTLY
            case MeasureSpec.EXACTLY:
                if (childWidth>=0){
                  //如果子view自己设置了具体的值,那么childWidth就是它自身的值,childMode就是EXACTLY
                    childWidth=childWidth;
                    childMode=MeasureSpec.EXACTLY;
                }else if (childWidth==LayoutParams.MATCH_PARENT){
                  //如果子view设置了MATCH_PARENT,那么它的childWidth就等于父view的widthSize,并且父view的mode也是EXACTLY,那么子view的mode也应该是EXACTLY
                    childWidth=widthSize;
                    childMode=MeasureSpec.EXACTLY;

                }else if (childWidth==LayoutParams.WRAP_CONTENT){
                  //如果子view设置了WRAP_CONTENT,那么这里先把父view的widthSize赋值给它的childWidth,并且它是可以在onMeasure()对自己的值进行修改的,那么它的值最大是widthSize,所以它的mode是MeasureSpec.AT_MOST
                    childWidth=widthSize;
                    childMode=MeasureSpec.AT_MOST;
                }
                break;
            //如果父view的要求是AT_MOST
            case MeasureSpec.AT_MOST:
                if (childWidth>=0){
            //如果子view自己设置了具体的值,那么childWidth就是它自身的值,childMode就是EXACTLY
                    childWidth=childWidth;
                    childMode=MeasureSpec.EXACTLY;
                }else if (childWidth==LayoutParams.MATCH_PARENT){
            //如果子view设置了MATCH_PARENT,那么它的childWidth就等于父view的widthSize,但是父view的mode是AT_MOST,那么子view的mode也应该是AT_MOST
                    childWidth=widthSize;
                    childMode=MeasureSpec.AT_MOST;
                }else if (childWidth==LayoutParams.WRAP_CONTENT){
            //如果子view设置WRAP_CONTENT,那么这里先把父view的widthSize赋值给它的childWidth,并且它是可以在onMeasure()对自己的值进行修改的,那么它的值最大是widthSize,所以它的mode是MeasureSpec.AT_MOST
                    childWidth=widthSize;
                    childMode=MeasureSpec.AT_MOST;
                }
                break;
            //如果父view没有要求
            case MeasureSpec.UNSPECIFIED:
                if (childWidth>=0){
            //如果子view自己设置了具体的值,那么childWidth就是它自身的值,childMode就是EXACTLY
                    childWidth=childWidth;
                    childMode=MeasureSpec.EXACTLY;
                }else if (childWidth==LayoutParams.MATCH_PARENT){
            //如果子view设置了MATCH_PARENT,那么它的childWidth就等于父view的widthSize,但是父view的mode是UNSPECIFIED,那么子view的mode也应该是UNSPECIFIED
                    childWidth=widthSize;
                    childMode=MeasureSpec.UNSPECIFIED;
                }else if (childWidth==LayoutParams.WRAP_CONTENT){
            //如果子view设置WRAP_CONTENT,那么这里先把父view的widthSize赋值给它的childWidth,并且它是可以在onMeasure()对自己的值进行修改的,那么它的值最大是widthSize,但是父view的widthSiz并没有限制,所以这里子view的mode也应该是UNSPECIFIED
                    childWidth=widthSize;
                    childMode=MeasureSpec.UNSPECIFIED;
                }
                break;
        
      //根据计算出的size和mode生成MeasureSpec
     int childWidthMeasureSpec=MeasureSpec.makeMeasureSpec(widthMode, widthSize);
     //height的步骤同理,这里省略
     int childWidthMeasureSpec=MeasureSpec.makeMeasureSpec(heightMode, HeightSize)
      //传入MeasureSpec完成测量
      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

以上就是子view的size和mode的计算,最后完成测量的过程,可以看出代码很繁琐,并且也是模版代码,Android也提供了方法:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                View child = getChildAt(0);
        //带margin的测量,一般用这种,这里的参数0是已用空间,可以通过计算已测量的子view进行计算,这里为了方便写为0
        measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,0);
        //普通测量子view
        measureChild(child,widthMeasureSpec,heightMeasureSpec);
        //测量所有子view,这里里面其实就是遍历了子view然后调用measureChild()
        measureChildren(widthMeasureSpec,heightMeasureSpec);
    }

这里有3个测量方法,其实内里逻辑差不多只是measureChildWithMargins()带了margin,那么在看下这个方法:

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        //这里用到了MarginLayoutParams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                //通过此方法完成mode和size的计算,这个方法不再展开
        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);
    }

可以看出内部也是计算出了MeasureSpec,最后通过measure()完成测量。需要主要的是如果使用measureChildWithMargins()测量,它里面用到了MarginLayoutParams,需要去实现一个方法,如果不实现,就无法使用layout_margin:

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }

小结:

1:自定义View时,可以在onMeasure()中通过resolveSize()对实际的宽高进行修正。如果不对宽高进行修改,那么就是它的父View调用child.measure()测量出的值,这里有个比较特殊的情况就是如果自身设置了wrap_content(),然后不做任何修正的话,那么父view的宽高就是自身的宽高,这个可以从上面的计算过程中看出来。

2:测量子View时可以通过measureChildWithMargins()等3个方法完成对子view的测量,需要注意的是如果使用measureChildWithMargins()需要实现generateLayoutParams()并返回new MarginLayoutParams(getContext(),attrs)

  • 2.2 再看layout():前面分析过,layout()第一个重要方法是setFrame():
  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;
                        //判断大小是否改变,布局改变不代表大小改变
          boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

                        //记录新的左上右下的值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
                        
            //如果布局大小改变,调用sizeChange(),然后在这个方法了里又会调用onSizeChanged()
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

        }
        return changed;
    }

这里如果如果布局发生了改变,那么再判断大小是否发生改变,然后给新的左上右下赋值

这里有一个点是左上右下位置的改变,并不代表大小改变,有可能只是位置改变,如果只是位置改变,大小不变那么不会触发onSizeChanged(),否则就会触发onSizeChanged(),并且onSizeChanged()执行时新的位置已经赋值完成,那么这时候这个布局的实际宽高已经有值了,也就是getWidth()和getHeight()可以获取到最新的宽高信息了。

setFrame()完后,如果布局发生了改变或者调用requestLayout(),那么就会调用onLayout(),这是一个空实现,可以在这个方法里通过child.layout(),去对子view进行布局,子view又会重复走以上layout()流程。

小结:layout()主要通过它的setFrame()方法判断了布局是否改变,并且记录了最新的左上右下的值,如果布局发生改变,并且大小改变,那么会调用onSizeChange(),所以一般如果不需要对子view进行操作的话,自身的操作可以放到这个方法里,因为这时已经可以通过getWidth()和getHeigh()拿到真实的宽高值了。如果需要对子view进行操作,那么就重写onLayout(),通过child.layout()去摆放子view的位置,如此一直循环

  • 2.3 再看onDraw():
    public void draw(Canvas canvas) {
        // Step 3, draw the content
        //绘制内容
        //背景不为空且不为透明时才会执行onDraw,viewgroup默认不会执行此方法
        if (!dirtyOpaque) onDraw(canvas)

    }

一般在onDraw()绘制自身内容,另外viewGroup默认不回执行onDraw(),同时onDraw()是一个会调用很多次的方法,所以不要在onDraw()做一些计算或频繁分配内存的工作,一面造成内存抖动。

3. 总结

  • view的绘制流程是从ViewRootImpl的performTraversals()开始的,在这个方法里依次调用了measure(),layout(),draw()

  • 自定义view时父view通过调用子view的measure()可以测量子view。然后在子view可以通过重写onMeasure(),对自己进行修改,修改完成后可以通过resolveSize()方法修正宽高,修正后再通过MeasureSpec.makeMeasureSpec()生成MeasureSpec,最后通过setMeasuredDimension()保存结果,这个方法后就可以通过getMeasuredWidth()和getMeasuredHeight()获取测量的宽高了。

  • 在onMeasure()调用子view的measure()方法可以对子view进行测量,可以通过measureChildWithMargins()生成需要的参数,调用这个方法必须实现generateLayoutParams()并返回new MarginLayoutParams(getContext(),attrs)

  • 测量完成后就是layout(),它先是调用setFrame(),在这个方法里如果左上右下的值任何一个改变,那么代表布局改变,同时会记录最新的值,再会判断布局大小是否改变,如果改变,就会调用onSizeChange(),一般如果布局大小改变需要进行的操作可以重写这个方法,它执行时已经能通过getHeight()和getWidth()获取实际的宽高了。

  • setFrame()执行完后会把结果返回给layout(),如果布局改变,那么会去执行onLayout(),这是个空方法,可以在这个方法里调用子view的layout()对子view进行布局。

  • 最后是onDraw()这个方法就是绘制具体内容的方法,所以它会频繁执行,所以需要避免在此频繁分配内存的操作。viewgroup默认不会执行ondraw()

附:invalidate()和requestLayout()的区别

  • invalidate()不会执行measure()和layout(),只执行draw(),如果开启了硬件加速则只进行调用者View的重绘。

  • requestLayout()会执行自身及父view的measure(),layout(),一般情况下不会执行draw流程,如果要重绘必须调用invalidate(),或者当布局大小发生改变时layout()中的setFrame()会触发重绘。子View不一定会重新measure()和layout()。

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

推荐阅读更多精彩内容