UI绘制流程

Activity的setContentView

setContentView(R.layout.activity_main);入手了解UI的绘制起始过程。
下面源码,是基于android-28下的。

1. Activity.java

getWindow() 拿到的是 Window 的唯一实现类 PhoneWindow。

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);// ①
    initWindowDecorActionBar();
}

2. PhoneWindow.java

在下面代码,②处创建 DecorView。

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();// ②
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);// ⑩
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

在下面代码,③处创建 DecorView,④处将 DecorView 转换为 ViewGroup。

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor(-1);// ③ 生成一个DecorView(继承的FrameLayout)
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);// ④
        ...
    }
}

下面可以看到调用了很多 setFlags()requestFeature() 这也就是为什么在 Activity 中,要将 getWindow().addFlags() 以及 getWindow().requestFeature() 放在 setContentView() 之前才会生效的原因。

protected ViewGroup generateLayout(DecorView decor) {// ⑤
    // Apply data from current theme.

    ...
    if (mIsFloating) {
        setLayout(WRAP_CONTENT, WRAP_CONTENT);
        setFlags(0, flagsToUpdate);
    } else {
        setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
    }

    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
        // Don't allow an action bar if there is no title.
        requestFeature(FEATURE_ACTION_BAR);
    }
    ...
    // Inflate the window decor.

    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
        setCloseOnSwipeEnabled(true);
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleIconsDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        // XXX Remove this once action bar supports these features.
        removeFeature(FEATURE_ACTION_BAR);
        // System.out.println("Title Icons!");
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
            && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
        // Special case for a window with only a progress bar (and title).
        // XXX Need to have a no-title version of embedded windows.
        layoutResource = R.layout.screen_progress;
        // System.out.println("Progress!");
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
        // Special case for a window with a custom title.
        // If the window is floating, we need a dialog layout
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogCustomTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_custom_title;
        }
        // XXX Remove this once action bar supports these features.
        removeFeature(FEATURE_ACTION_BAR);
    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
        // If no other features and not embedded, only need a title.
        // If the window is floating, we need a dialog layout
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                    R.attr.dialogTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
            layoutResource = a.getResourceId(
                    R.styleable.Window_windowActionBarFullscreenDecorLayout,
                    R.layout.screen_action_bar);
        } else {
            layoutResource = R.layout.screen_title;
        }
        // System.out.println("Title!");
    } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        layoutResource = R.layout.screen_simple_overlay_action_mode;
    } else {
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
        // System.out.println("Simple!");
    }

    mDecor.startChanging();
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);// ⑥
    // 找到id为com.android.internal.R.id.content的固定ViewGroup
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);// ⑨
    ...
    return contentParent;
}

上面的 layoutResource,是根据是否存在 ActionBar 之类的一些控件判断,进行赋值,使用系统默认资源文件,在 sdk/platforms/android-28/data/res 目录下。

3. DecorView.java

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    ...
    mDecorCaptionView = createDecorCaptionView(inflater);
    final View root = inflater.inflate(layoutResource, null);// ⑦
    if (mDecorCaptionView != null) {
        if (mDecorCaptionView.getParent() == null) {
            addView(mDecorCaptionView,
                    new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mDecorCaptionView.addView(root,
                new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
    } else {

        // Put it below the color views.
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));// ⑧
    }
    mContentRoot = (ViewGroup) root;
    ...
}

在⑦处,将系统默认的布局转换为 View,在⑧处,addView() 时添加到 DecorView 中。在⑨处,找到 id 为 content 的 ViewGroup,最后执行到 PhoneWindow 代码⑩处,将Activity中设置的布局添加到 mContentParent 中,完成了这一套流程,见下面两张图。

Activity加载显示基本流程

Activity加载UI-类图关系和视图结构

需要注意的是在7.0以后,DecorView 不再是 PhoneWindow 的内部类,上图基于android-23源码。

我们总结下 View 是如何被添加到屏幕窗口上的。

  1. 创建顶层布局容器 DecorView
  2. 在顶层布局中加载基础布局 ViewGroup
  3. 将 ContentView 添加到基础布局中的 FrameLayout 中

measure、layout、draw 的执行流程

  • measure:测量,测量自己有多大,如果是ViewGroup的话会同时测量里面的子控件的大小。
  • layout:摆放里面的子控件 bounds (left, top, right, bottom)。
  • draw:绘制(直接继承了View一般都会重写onDraw)。

measure 的流程

View 绘制流程
DecorView添加到窗口Window的过程
  1. 我们从 ViewRootImpl 类的 requestLayout() 方法看起( ViewRootImpl 是 PhoneWindow 和 DecorView 的桥梁),再之前调用大家可以自行去看源码。
@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();// 检查是否是在UI线程中执行
        mLayoutRequested = true;
        scheduleTraversals();// 关键代码
    }
}
  1. scheduleTraversals 中的 mTraversalRunnable 中做了关键操作。
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
  1. mTraversalRunnable 实现了 Runnable,执行了 doTraversal()
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
  1. 在这里调用里 performTraversals()
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}
  1. 继而完成了后续的performMeasure()performLayout()performDraw()
private void performTraversals() {
    ...
    // Ask host how big it wants to be
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    performLayout(lp, mWidth, mHeight);
    ...
    performDraw();
    ...
}
performTraversals方法控制View绘制流程图
  1. 最终 performMeasure() 调用了 View 的 measure()
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);
    }
}
  1. measure() 会调用 onMeasure(),之后的流程分析可以看这里,自定义控件中,measure的流程
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}

performLayout()performDraw() 的流程和 performMeasure()也是类似的。

ViewGroup 绘制流程

首先思考两个问题:如何去合理的测量一颗 View 树?如果 ViewGroup 和 View 都是直接指定的宽高,我还要测量吗?

正是因为谷歌设计的自适应尺寸机制(比如 match_parent , wrap_content),造成了宽高不确定,所以就需要进程测量 measure 过程。measure 过程会遍历整颗 View 树,然后依次测量每一个 View 的真实的尺寸。(树的遍历--先序遍历)

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}

measure 方法中调用了 onMeasure。

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

一般来说,继承 ViewGroup 的子类,会重写 onMeasure() ,对子控件进行测量,可以自行参考下 FrameLayout 和 DecorView 的 onMeasure(),这个在里面,就会计算 MeasureSpec 了。


MeasureSpec:测量规格,int 值,32 位,拿前面两位当做 mode,后面 30 位当做值。

  1. mode:有三种模式。
  • EXACTLY:精确的。比如给了一个确定的值 100dp;再比如果父控件是 EXACTLY 确定值,子控件 match_parent 也属于精确值。
  • AT_MOST:根据父容器当前的大小,结合你指定的尺寸参考值来考虑你应该是多大尺寸,需要计算。比如 wrap_content(如果父控件是 AT_MOST 或者 UNSPECIFIED 不确定,子控件 match_parent 也属于这种)。
  • UNSPECIFIED:未指定的意思。根据当前的情况,结合你制定的尺寸参考值来考虑,在不超过父容器给你限定的尺寸的前提下,来测量你的一个恰好的内容尺寸。用的比较少,一般见于系统使用 ScrollView,ListView(大小不确定,同时大小还是变的。会通过多次测量才能真正决定好宽高。)

对于上面的结论,我们也可以通过看 FrameLayout 的源码分析出来。

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;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } 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;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

最后总结两张表格:


getChildMeasureSpec方法分析

View 的 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 决定。


  1. value:宽高的值。

  2. 从 MeasureSpec 中获取 mode 和 value:

final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  1. 将 mode 和 value 合成一个 MeasureSpec:
MeasureSpec.makeMeasureSpec(resultSize, resultMode);

FrameLayout 的 onMeasure 方法内,还会调用 child.measure()

View树的源码measure流程图

经过大量测量以后,最终确定了自己的宽高,需要调用View:setMeasuredDimension(w, h)

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(w, h) ,保存 measuredWidth 和 measuredHeight。

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

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

到这里,就可以理解,写自定义控件的时候,我们要去获得自己的宽高来进行一些计算,必须先经过 measure(),才能获得到宽高,这里说的宽高不是 getWidth(),而是 getMeasuredWidth()getWidth()layout() 之后才可以拿到值。

同样的,当我们重写 onMeasure 的时候,我们需要在里面调用 child.measure() 才能获取 child 的宽高,进而去计算我们自定义 ViewGroup 的宽高。

设计 ViewGroup 的目的是什么?

  1. 作为容器处理焦点问题。
  2. 作为容器处理事件分发问题。
  3. 控制容器添加View的流程:addView()removeView()
  4. 抽出了一些容器的公共的工具方法:measureChildren()measureChild()measureChildWithMargins() 方法。
layout 的流程

从 ViewRootImpl 的 performLayout() 看起。

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
        int desiredWindowHeight) {
    ...省略代码...
    final View host = mView;
    if (host == null) {
        return;
    }
    ...省略代码...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...省略代码...
}

上面的 host 就是 DecorView,调用了 View 的 layout()

public void layout(int l, int t, int r, int b) {
    ...省略代码...
    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);
    }
}

onLayout() 是一个空实现,如果是 ViewGroup 需要重写该方法,在这里面对子控件进行摆放。

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

总结:

  1. 调用 layout() 确定自身的位置,也就是左、上、右、下,四个点的位置。
  2. 如果是 ViewGroup,还需要调用 onLayout() 确定子 View 的位置(View不需要) 。
draw 的流程

依然从 ViewRootImpl 的 performDraw() 看起,这里面的代码太多,就不进行粘贴了,只描述下重要方法,在 ViewRootImpl 中 从performDraw()->draw()->drawSoftware(),最终在这方法里面,调用了 View 的 draw()

View 源码 draw() 中的注释已经说的很详细。

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)

值得注意的一点是,ViewGroup 默认是不需要执行第3步的,也就是 ViewGroup 的 onDraw 方法默认是不会调用的。这个也比较好理解 LinearLayout 和 RelativeLayout 这些容器,自身没有任何可以绘制的,只需要绘制 children 就可以了,这样就是为了提高性能和效率。
在 ViewGroup 的构造方法中可以看到,调用了 initViewGroup(),简单看下源码:

private void initViewGroup() {
    // ViewGroup doesn't draw by default
    if (!debugDraw()) {
        setFlags(WILL_NOT_DRAW, DRAW_MASK);// 设置不绘制自身
    }
    ...省略代码...
}

如果需要 ViewGroup 执行 onDraw() ,可以在构造方法中执行如下代码:

setWillNotDraw(false);

或者设置背景,即使是透明背景,都是可以执行 onDraw()

android:background="@android:color/transparent"

但是这样是是不可以的:

android:background="@null"

如果是 ViewGroup ,还会执行第4步,ViewGroup 中实现了 dispatchDraw(),完成对子控件的绘制。

总结 View:

  1. 绘制背景 drawBackground(canvas)
  2. 绘制自己 onDraw(canvas)
  3. 绘制前景,滚动条等装饰 onDrawBackground(canvas)

总结 ViewGroup:

  1. 绘制背景 drawBackground(canvas)
  2. 绘制自己 onDraw(canvas)
  3. 绘制子 View dispatchDraw(canvas),ViewGroup 已经帮我们实现好了。
  4. 绘制前景,滚动条等装饰 onDrawBackground(canvas)

思考

思考:如何让一个 ScrollView 里面的 ListView 全部展开?
有一种解决办法就是继承 ListView,重写 onMeasure 方法:

public void onMeasure() {
    // 第一个参数是value,第二个参数是mode,通过makeMeasureSpec工具类,合并为一个int
    int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, expandSpec); 
}

为什么要这么做?

  1. 设置 mode 为 MeasureSpec.AT_MOST?
  2. 设置 value 为 Integer.MAX_VALUE >> 2?

答案在这里

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

推荐阅读更多精彩内容