View绘制流程(二)

View绘制流程(一)

View的布局

ViewRootImplperformTraversalsperformMeasure执行完成以后会接着执行performLayoutViewRootImpl调用performLayout执行Window对应的View的布局。

  • ViewRootImpl的performLayout。
  • DecorView(FrameLayout)的layout方法。
  • DecorView(FrameLayout)的onLayout方法。
  • DecorView(FrameLayout)的layoutChildren方法。
  • DecorView(FrameLayout)的所有子View的Layout。
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    /*******部分代码省略**********/

    //View的布局
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        /*******部分代码省略**********/
        final View host = mView;
        /*******部分代码省略**********/
        try {
            //调用View的Layout方法进行布局
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            mInLayout = false;
            //在ViewRootImpl进行布局的期间,Window内的View自己进行requestLayout
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {           
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
                        //请求对该View布局,最终回调到ViewRootImpl的requestLayout进行重新测量、布局、绘制
                        view.requestLayout();
                    }
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
                    mHandlingLayoutInLayoutRequest = false;

                    // Check the valid requests again, this time without checking/clearing the
                    // layout flags, since requests happening during the second pass get noop'd
                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    if (validLayoutRequesters != null) {
                        getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);
                                    /*******部分代码省略**********/
                                    //请求对该View布局,最终回调到ViewRootImpl的requestLayout进行重新测量、布局、绘制
                                    view.requestLayout();
                                }
                            }
                        });
                    }
                }
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }
}

layout方法接收四个参数,这四个参数分别代表相对Parent的左、上、右、下坐标。而且还可以看见左上都为0,右下分别为上面刚刚测量的widthheight

public void layout(int l, int t, int r, int b) {
        ......
        //实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量
        //判断View的位置是否发生过变化,以确定有没有必要对当前的View进行重新layout
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //需要重新layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //回调onLayout
            onLayout(changed, l, t, r, b);
            ......
        }
        ......
}

类似measure过程,layout调用了onLayout方法,这里需要注意的是layout方法可以被子类重写,下面看一下ViewonLayout方法。

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

竟然是一个空的方法。,那么就看一下ViewGrouplayout方法。

@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);
        }
        //调用View的layout方法
        super.layout(l, t, r, b);
    } else {
        mLayoutCalledWhileSuppressed = true;
    }
}

本质还是调用Viewlayout方法,这里需要注意的是ViewGroup的layout方法是不能被子类重写的。

接下来看下ViewGrouponLayout方法。

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

ViewGrouponLayout()方法竟然是一个抽象方法,这就是说所有ViewGroup的子类都必须重写这个方法。所以在自定义ViewGroup控件中,onLayout配合onMeasure方法一起使用可以实现自定义View的复杂布局。自定义View首先调用onMeasure进行测量,然后调用onLayout方法动态获取子View和子View的测量大小,然后进行layout布局。重载onLayout的目的就是安排其children在父View的具体位置,重载onLayout通常做法就是写一个for循环调用每一个子视图的layout(l, t, r, b)函数,传入不同的参数l, t, r, b来确定每个子视图在父视图中的显示位置。

既然ViewonLayout方法为空方法,ViewGrouponLayout方法为抽象方法,下面以ViewGroup的子类LinearLayout为例。

public class LinearLayout extends ViewGroup {
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
}

LinearLayoutlayout过程是分VerticalHorizontal的,这个就是xml布局的orientation属性设置的。

void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;

    // Where right end of child should go
    //计算父窗口推荐的子View宽度
    final int width = right - left;
    //计算父窗口推荐的子View右侧位置
    int childRight = width - mPaddingRight;

    // Space available for child
    //child可使用空间大小
    int childSpace = width - paddingLeft - mPaddingRight;
    //通过ViewGroup的getChildCount方法获取ViewGroup的子View个数
    final int count = getVirtualChildCount();
    //获取Gravity属性设置
    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    //依据majorGravity计算childTop的位置值
    switch (majorGravity) {
       case Gravity.BOTTOM:
           // mTotalLength contains the padding already
           childTop = mPaddingTop + bottom - top - mTotalLength;
           break;

           // mTotalLength contains the padding already
       case Gravity.CENTER_VERTICAL:
           childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
           break;

       case Gravity.TOP:
       default:
           childTop = mPaddingTop;
           break;
    }
    //重点!!!开始遍历
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            //LinearLayout中其子视图显示的宽和高由measure过程来决定的,因此measure过程的意义就是为layout过程提供视图显示范围的参考值
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            //获取子View的LayoutParams
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

            int gravity = lp.gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            //依据不同的absoluteGravity计算childLeft位置
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp.leftMargin - lp.rightMargin;
                    break;

                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp.rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin;
            //通过垂直排列计算调运child的layout设置child的位置
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

实质都是调用setFrame方法把参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。

从上面分析的ViewGroup子类LinearLayout的onLayout实现代码可以看出,一般情况下layout过程会参考measure过程中计算得到的mMeasuredWidthmMeasuredHeight来安排子View在父View中显示的位置,但这不是必须的,measure过程得到的结果可能完全没有实际用处,特别是对于一些自定义的ViewGroup,其子View的个数、位置和大小都是固定的,这时候我们可以忽略整个measure过程,只在layout方法中传入的4个参数来安排每个子View的具体位置。

到这里就不得不提getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()这两对方法之间的区别(上面分析measure过程已经说过getMeasuredWidth()、getMeasuredHeight()必须在onMeasure之后使用才有效)。可以看出来getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效。那我们看下View源码中这些方法的实现吧,如下:

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

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

public final int getWidth() {
    return mRight - mLeft;
}

public final int getHeight() {
    return mBottom - mTop;
}

public final int getLeft() {
    return mLeft;
}

public final int getRight() {
    return mRight;
}

public final int getTop() {
    return mTop;
}

public final int getBottom() {
    return mBottom;
}

mMeasuredWidth是一个8位的十六进制数,高两位代表ModeMEASURED_SIZE_MASK按位&后获得测量后的宽度。

View布局总结

View.layout方法可被重载,ViewGroup.layoutfinal的不可重载,ViewGroup.onLayoutabstract的,子类必须重载实现自己的位置逻辑。View.onLayout方法是一个空方法。

measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeightlayout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

onLayout中最终循环调用子ViewsetFrame方法来设置mLeft、mTop、mRight和mBottom的值。

getMeasuredWidth()、getMeasuredHeight()必须在onMeasure之后使用才有效;getWidth()与getHeight()方法必须在layout(int l, int t, int r, int b)执行之后才有效。

View的绘制

ViewRootImpl调用performDraw执行Window对应的View的布局。

  • ViewRootImpl的performDraw
  • ViewRootImpl的draw
  • ViewRootImpl的drawSoftware
  • DecorView(FrameLayout)的draw方法;
  • DecorView(FrameLayout)的dispatchDraw方法;
  • DecorView(FrameLayout)的drawChild方法;
  • DecorView(FrameLayout)的所有子View的draw方法;
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    /*******部分代码省略**********/
    //View的绘制
    private void performDraw() {
    /*******部分代码省略**********/
        try {
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        /*******部分代码省略**********/
    }
    //进行绘制
    private void draw(boolean fullRedrawNeeded) {
        /*******部分代码省略**********/
        //View上添加的Observer进行绘制事件的分发
        mAttachInfo.mTreeObserver.dispatchOnDraw();
        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
                /*******部分代码省略**********/
                //调用Window对应的ViewRootImpl的invalidate方法
                mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
            } else {
                /*******部分代码省略**********/
                //绘制Window
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
            }
        }

        if (animating) {
            mFullRedrawNeeded = true;
            scheduleTraversals();
        }
    }

    void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            scheduleTraversals();
        }
    }

    /**
     * @return true if drawing was successful, false if an error occurred
     */
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

        /*******部分代码省略**********/
        try {
            /*******部分代码省略**********/
            try {
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;
                //View绘制
                mView.draw(canvas);

                drawAccessibilityFocusedDrawableIfNeeded(canvas);
            } finally {
                if (!attachInfo.mSetIgnoreDirtyState) {
                    // Only clear the flag if it was not set during the mView.draw() call
                    attachInfo.mIgnoreDirtyState = false;
                }
            }
        } finally {
            /*******部分代码省略**********/
        }
        return true;
    }
}

由于ViewGroup没有重写Viewdraw方法,所以直接看View.draw方法:

public void draw(Canvas canvas) {
    ......
    // 1. 绘制背景
    ......
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    ......
    //2.绘制View的内容
    if (!dirtyOpaque) onDraw(canvas);

    //3.对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制。
    dispatchDraw(canvas);
    ......
    //4.对View的滚动条进行绘制。
    onDrawScrollBars(canvas);
    ......
}
1.对View的背景进行绘制。
private void drawBackground(Canvas canvas) {
    //获取xml中通过android:background属性或者代码中setBackgroundColor()、setBackgroundResource()等方法进行赋值的背景Drawable
    final Drawable background = mBackground;
    ......
    //根据layout过程确定的View位置来设置背景的绘制区域
    if (mBackgroundSizeChanged) {
        background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
    ......
        //调用Drawable的draw()方法来完成背景的绘制工作
        background.draw(canvas);
    ......
}

可以看出View背景是一个Drawable,绘制背景最终调用的是Drawable.draw

2.对View的内容进行绘制。
protected void onDraw(Canvas canvas) {
}   

View.onDraw方法为空方法,ViewGroup也没有重写该方法。因为每个View的内容部分是各不相同的,所以需要由子类去实现具体逻辑。

3.对当前View的所有子View进行绘制,如果当前的View没有子View就不需要进行绘制。

View.dispatchDraw()方法是一个空方法,如果View包含子类需要重写他,所以我们看下ViewGroup.dispatchDraw方法源码:

@Override
protected void dispatchDraw(Canvas canvas) {
    ......
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    ......
    for (int i = 0; i < childrenCount; i++) {
        ......
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ......
    // Draw any disappearing views that have animations
    if (mDisappearingChildren != null) {
        ......
        for (int i = disappearingCount; i >= 0; i--) {
            ......
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ......
}

该方法内部会遍历每个子View,然后调用drawChild()方法

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}
4.对View的滚动条进行绘制。
protected final void onDrawScrollBars(Canvas canvas) {
    final ScrollabilityCache cache = mScrollCache;
    if (cache != null) {
        int state = cache.state;
        if (state == ScrollabilityCache.OFF) {
            return;
        }
        boolean invalidate = false;   
        .........  
        final boolean drawHorizontalScrollBar = isHorizontalScrollBarEnabled();
        final boolean drawVerticalScrollBar = isVerticalScrollBarEnabled()
                && !isVerticalScrollBarHidden();
        // Fork out the scroll bar drawing for round wearable devices.
        if (mRoundScrollbarRenderer != null) {
            if (drawVerticalScrollBar) {
                final Rect bounds = cache.mScrollBarBounds;
                getVerticalScrollBarBounds(bounds, null);
                mRoundScrollbarRenderer.drawRoundScrollbars(
                        canvas, (float) cache.scrollBar.getAlpha() / 255f, bounds);
                if (invalidate) {
                    invalidate();
                }
            }
        } else if (drawVerticalScrollBar || drawHorizontalScrollBar) {
            final ScrollBarDrawable scrollBar = cache.scrollBar;
            if (drawHorizontalScrollBar) {
                scrollBar.setParameters(computeHorizontalScrollRange(),
                        computeHorizontalScrollOffset(),
                        computeHorizontalScrollExtent(), false);
                final Rect bounds = cache.mScrollBarBounds;
                getHorizontalScrollBarBounds(bounds, null);
                onDrawHorizontalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                        bounds.right, bounds.bottom);
                if (invalidate) {
                    invalidate(bounds);
                }
            }
            if (drawVerticalScrollBar) {
                scrollBar.setParameters(computeVerticalScrollRange(),
                        computeVerticalScrollOffset(),
                        computeVerticalScrollExtent(), true);
                final Rect bounds = cache.mScrollBarBounds;
                getVerticalScrollBarBounds(bounds, null);
                onDrawVerticalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                        bounds.right, bounds.bottom);
                if (invalidate) {
                    invalidate(bounds);
                }
            }
        }
    }
}

可以看见其实任何一个View都是有(水平垂直)滚动条的,只是一般情况下没让它显示而已。

View的invalidate和postInvalidate方法源码分析

View中的invalidate方法有很多的重载,最终都会调用invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate)方法(ViewGroup没有重写这些方法)

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
    ......
        // Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            //设置刷新区域
            damage.set(l, t, r, b);
            //传递调运Parent ViewGroup的invalidateChild方法
            p.invalidateChild(this, damage);
        }
        ......
}

View.invalidateInternal方法实质是将要刷新区域直接传递给了父ViewGroup的invalidateChild方法,在该方法中,调用父View的invalidateChild,这是一个从当前向上级父View回溯的过程,每一层的父View都将自己的显示区域与传入的刷新Rect做交集 。所以我们看下ViewGroup的invalidateChild方法,源码如下:

public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;
    final AttachInfo attachInfo = mAttachInfo;
    ......
    do {
        ......
        //循环层层上级调运,直到ViewRootImpl会返回null
        parent = parent.invalidateChildInParent(location, dirty);
        ......
    } while (parent != null);
}

这个过程最后传递到ViewRootImpl的invalidateChildInParent方法结束,所以我们看下ViewRootImpl的invalidateChildInParent方法,如下:

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    ......
    //View调运invalidate最终层层上传到ViewRootImpl后最终触发了该方法
    scheduleTraversals();
    ......
    return null;
}

最终调用了scheduleTraversals方法,该方法会调用ViewRootImpl.performTraversals方法,重新绘制。

invalidate该方法只能在UI Thread中执行,其他线程中需要使用postInvalidate方法

postInvalidate方法最终会调用ViewRootImpl类的dispatchInvalidateDelayed方法,源码如下:

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
    Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
    mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

ViewRootImpl类的Handler发送了一条MSG_INVALIDATE消息,继续追踪这条消息的处理可以发现:

public void handleMessage(Message msg) {
    ......
    switch (msg.what) {
    case MSG_INVALIDATE:
        ((View) msg.obj).invalidate();
        break;
    ......
    }
    ......
}

最终在UI线程中调用了Viewinvalidate方法。

直接调用invalidate方法.请求重新draw,但只会绘制调用者本身。因为其他的View状态没有变化的话,是不会执行对应的绘制方法的。

在窗口上的View都有一个ViewRootImpl作为它的Parent,处理View的布局、事件处理等。

Kotlin项目实战

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

推荐阅读更多精彩内容