View测量、布局及绘制原理

ActivityThread.handleResumeActivity

1 、View绘制的三大过程

//View绘制的三大过程开始位置
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
                                 String reason) {
// 将DecorView添加到Window上,紧接着进入绘制三大过程,实际上是调用WindowManagerImpl的addView方法,然后调用WindowManagerGlobal的addView方法。
                // 出发绘制的单打过程的条件是:当DecorView被添加到Window中时。
                wm.addView(decor, l);

}

这块是什么意思呢?通俗讲就是就是setContentView源码理解将开发者定义XML布局解析或构建成Android的View树,当Activity声明周期执行到handleResumeActivity时,把根View添加到窗口中,紧接着将根View交由ViewRootImpl管理绘制等。这样我们就把Activity.onCreate中setContentView和View的绘制流程串联起来了。
当然这里面会构建很多关于View绘制过程所需的参数,比如:LayoutParams 等。

WindowManagerImpl.addView
 // 参数view是顶层视图(DecorView)
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
WindowManagerGlobal
 // 参数view是顶层视图(DecorView)
 public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

ViewRootImpl root;
View panelParentView = null;

root = new ViewRootImpl(view.getContext(), display);
// view就是顶层视图(DecorView)
view.setLayoutParams(wparams);
// 将View和和ViewRooot已将View的 params收集到容器中管理
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);

try {

 root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
        }
}
小结

我省略了一些不重要的代码,触发View的绘制过程的条件是ActivityTherad.handleResumeActivity方法开始将DecorView添加到Window上时,紧接着在WindowManagerGlobal的addView方法中创建ViewRootImpl 对象并调用ViewRootImpl的setView方法,将顶层视图(DecorView)做参数传入,进入绘制三大过程,实际上是调用WindowManagerImpl的addView方法,然后调用WindowManagerGlobal的addView方法。触发View绘制三大过程的条件是:当DecorView被添加到Window中时,上面最后一步提到调用ViewRootimpl的setView并传入DecorView,那么我们看一下ViewRootImpl.setView方法做了什么?

ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
            requestLayout();
}

我们看到setView方法里面调用了requestLayout方法,代码如下:

 @Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

在requestLayout方法中又调用 checkThread(),看看就知道检查当前线程,看一下代码:

 void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

你现在应该知道为什么不能再工作线程中更新UI了吗?当然你要是Activity的onResume之前是可以在工作线程更新UI的,我们看到在requestLayout方法中还调用checkThread结束还调用scheduleTraversals方法,点击去看看代码:

void scheduleTraversals() {

     mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

}

在scheduleTraversals方法中其他的不要看,你就mTraversalRunnable代码。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

在TraversalRunnable 中又调用doTraversal方法。

void doTraversal() {

  performTraversals();

}

可以看到在doTraversal方法中调用performTraversals方法。

private void performTraversals() {

final View host = mView;

// 因为顶层View,这里需要根据窗口的宽高以及View自身的LayoutParams计算MeasureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                
// performMeasure  测量
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
//再次测量的标志
boolean measureAgain = false;
//有权重,就会被测量两次,为了性能,想线性布局中使用Weight
if (lp.horizontalWeight > 0.0f) {
    width += (int) ((mWidth - width) * lp.horizontalWeight);
    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
    measureAgain = true;
 }
if (lp.verticalWeight > 0.0f) {
   height += (int) ((mHeight - height) * lp.verticalWeight);
   childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
   measureAgain = true;
}
if (measureAgain) {
    //有权重,就会被测量两次,为了性能,想线性布局中使用Weight
   // performMeasure
   performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// performLayout
 performLayout(lp, mWidth, mHeight);


// performDraw
 performDraw();

}

可以看到在performTraversals方法中又分别调用:performMeasure(测量)、performLayout(布局)和performDraw(绘制),执行了View的绘制三大过程,那么接下来我们分别介绍这些过程。

2、 测量(performMeasure)

首先看看performTraversals方法在执行performMeasure之前做的测量准备,代码如下;

// 因为顶层View,这里需要根据窗口的宽高以及View自身的LayoutParams计算MeasureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                
// performMeasure  测量
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
//再次测量的标志
boolean measureAgain = false;
//有权重,就会被测量两次,为了性能,想线性布局中使用Weight
if (lp.horizontalWeight > 0.0f) {
    width += (int) ((mWidth - width) * lp.horizontalWeight);
    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
    measureAgain = true;
 }
if (lp.verticalWeight > 0.0f) {
   height += (int) ((mHeight - height) * lp.verticalWeight);
   childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
   measureAgain = true;
}
if (measureAgain) {
    //有权重,就会被测量两次,为了性能,想线性布局中使用Weight
   // performMeasure
   performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}

首先我知道测量时需要MeasureSpec,所以在performTraversals方法中执行performMeasure方法之前,即在测量之前需要准备MeasureSpec,具体代码:

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

我们看看那getRootMeasureSpec方法:

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

可以看到在创建DecorView的MeasureSpec时,需要根据自己的LayoutParams和Parent的测量模式(model)最终决定DecorView的MeasureSpec。

2.1 、MeasureSpec的介绍

模式(Mode) + 尺寸(Size)->MeasureSpec  32位int值
00000000 00000000 00000000 00000000
SpecMode(前2位)   +  SpecSize(后30)
mode + size --> MeasureSpec

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

父容器不对View做任何限制,系统内部使用
public static final int UNSPECIFIED = 0 << MODE_SHIFT; 00000000 00000000 00000000 00000000

父容器检测出View的大小,Vew的大小就是SpecSize LayoutPamras match_parent 固定大小
public static final int EXACTLY     = 1 << MODE_SHIFT; 01000000 00000000 00000000 00000000

父容器指定一个可用大小,View的大小不能超过这个值,LayoutPamras wrap_content
public static final int AT_MOST     = 2 << MODE_SHIFT; 10000000 00000000 00000000 00000000

由上述可知MeasureSpe封装了测量model和size,而前2位表示SpecMode,后30位表示(SpecSize),MeasureSpe中还定义了三种MeasureSpe,分别是:

  • UNSPECIFIED 父容器不对View做任何限制,系统内部使用,当然你在ScrollView中也是可以看到的。
  • EXACTLY 父容器检测出View的大小,Vew的大小就是SpecSize LayoutPamras.match_parent 固定大小
  • AT_MOST 父容器指定一个可用大小,View的大小不能超过这个值,LayoutPamras.wrap_conten

在MeasureSpe中定义了从MeasureSpe获取model和size的方法,我们就不看了。

2.2、performMeasure方法
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    try {
        //调用DecorView的measure方法进行调度测量,那么DecorView是继承了FrameLayout
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

可以看到performMeasure方法中直接调用了View.measure,而这个measure方法是View的测量调度方法:

2.2.1 默认的情况
//View的measure方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        resolveRtlPropertiesIfNeeded();

        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        /**
         * 没有缓存直接调用onMeasure进行测量操作
         *  onMeasure  -> setMeasuredDimension -> setMeasuredDimensionRaw(保存测量的结果)
         */
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            // 测量自己,这应该将mPrivateFlags设置为PFLAG_MEASURED_DIMENSION_SET标志回传
            // 不复写使用默认大小,因为目前是针对的DecorView,所以我们要看DecorView的OnMeasure的实现,即Framelayout
            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;
        }

        // flag not set, setMeasuredDimension() was not invoked, we raise
        // an exception to warn the developer
        /**
         * 在setMeasuredDimensionRaw中设置 将mPrivateFlags为了PFLAG_MEASURED_DIMENSION_SET标志
         */
        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()");
        }

        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }

}



protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //不复写使用默认大小
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}


protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

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

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

为了简洁我把一些优化的算法给删掉,在measure方法中又调用onMeasure测量自己,然后调用setMeasuredDimension和setMeasuredDimensionRaw 方法保存测量结果,并将mPrivateFlags设置为PFLAG_MEASURED_DIMENSION_SET标志回传,如果你不复写View.onMeasure方法,就使用默认大小,如果你自定义View,不复写onMeasure方法,最后你会发现:wrap_content 和 match_parent的效果是一样的,可以进去看看onMeasure源码,因为目前是针对的DecorView,所以我们要看DecorView的OnMeasure的实现,即Framelayout,当你需要自定义Viewroup时你需要根据child的布局属性来测量自己的宽和高,而当你自定义View时,你仅仅只需要测量自己就可以了,最后总结:

ViewGroup : measure --> onMeasure(测量子控件的宽高measureChild)--> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)
View measure --> onMeasure(测量自己) --> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的宽高)

最后我们来看看FrameLayout的onMeasure是怎么实现的:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

    final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        // GONE 是不进行测量的
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            /**
             * 调用measureChildWithMargins测量Child
             */
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);


            /**
             * 根据FrameLayout的布局特点,为了计算自己的大小,它只需要计算最大的宽度和高度
             */
            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);
    //设置测量结果,进行回传
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
}

 // 测量child  measureChildWithMargins方法
 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);
}

可以看到在FrameLayout.onMeasure方法中是通过循环遍历所有的child,来决定自己的最终宽度和高,那么DecorView是继承FrameLayout的,所以这也就是DecorView的测量规范,由measureChildWithMargins方法得知Child的ModeSpec是由parentWidthMeasureSpec 和 Child的LayoutParam决定,然后在顶用child.measure方法将计算出来的ModeSpec传给Child,最终会回调到child的onMeasure方法中。

3、布局 performLayout

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,  int desiredWindowHeight) {

      //View和ViewGroup 也是会调用的onLayout,layout方法是摆放自己,而onLayout是根据自己的要求摆放child的
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}

顶层视图(DecorView)host调用layout方法摆放自己

public void layout(int l, int t, int r, int b) {

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

在layouyt方法中有会调用onLayout,对child进行摆放,不管是View还是ViewGroup都会回调onLayout方法,但是onLayout方法是对child进行摆放的,在View中是空实现,你自定义实现了也没有实际的意义,所以layout的过程是比较简单的,接下来我们看看performDraw方法,
ViewGroup.layout(来确定自己的位置,4个点的位置) -->onLayout(进行子View的布局)
View.layout(来确定自己的位置,4个点的位置)。

4、绘制performDraw

 private void performDraw() {
       boolean canUseAsync = draw(fullRedrawNeeded);
  }

performDraw方法很简单,直接调用了draw方法:

 private boolean draw(boolean fullRedrawNeeded) {
           if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                    scalingRequired, dirty, surfaceInsets)) {
                return false;
            }
 }

draw方法代码很多,我就挑重点讲,在draw方法中调用了drawSoftware方法:

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
    mView.draw(canvas);
}

drawSoftware方法代码非常多,主要就是初始化Canvas相关的操作,只挑重点讲,在drawSoftware方法中经过Canvas的初始化,直接调用了View的mView.draw(canvas);并将canvas作为参数传进去:

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;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
      //ViewGroup不会回调onDraw这个方法,如果你设置背景onDraw是会被回调的,具体请看ViewGroup的构造方法initViewGroup
        if (!dirtyOpaque) onDraw(canvas);


     //在ViewGroup实现了这个方法,在View中是空实,意思就是:如果是ViewGroup那么它会分发绘制的给child,而绘制有child自己完成。
        dispatchDraw(canvas);
   

       // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
 }

在draw方法中,你可能会看到privateFlags 这个标志,实际上这个表示是用来表示是否需要绘制,即回调onDraw方法,在ViewGroup的构造函数中将此标志设置为 WILL_NOT_DRAW,所以为什么ViewGroup默认不会回调onDraw方法的原因,当然如果你设置背景onDraw是会被回调的。

最后关于自定义View相关的:

  • ViewGroup
    1、绘制背景 drawBackground(canvas);

    2、绘制自己onDraw(canvas),实际上ViewGroup 并不需要去关心这个方法;

    3、绘制子View dispatchDraw(canvas) ViewGroup 会在dispatchDraw方法中循环遍历所有的child并调用ViewGroup .drawChild方法紧接着调用child.draw方法让child自己绘制,dispatchDraw方法是在ViewGroup中才会有的,所以View并不存在此方法;

    4、绘制前景,滚动条等装饰onDrawForeground(canvas)

  • View
    1、绘制背景 drawBackground(canvas)

2、绘制自己onDraw(canvas)

3、绘制前景,滚动条等装饰onDrawForeground(canvas)

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

推荐阅读更多精彩内容