Android View繪制流程

前言

不知道大家有沒有想過一個問題谐区,當(dāng)啟動一個Activity的時候湖蜕,相應(yīng)的XML布局文件中的View是如何顯示到屏幕上的?有些同學(xué)會說是通過onMeasure()宋列、onLayout()昭抒、onDraw()這3個方法來完成的,實際上這只是系統(tǒng)暴露給我們使用的最基本的方法,背后的流程要比這個更加復(fù)雜灭返,今天就和大家一起扒一下背后還做了什么事情盗迟。

我們知道Activity執(zhí)行了onCreate()onStart()熙含、 onResume()3個方法后罚缕,用戶就能看見視圖了。這背后實際上經(jīng)歷了2個過程怎静,其一邮弹,通過ActivityThread調(diào)度生命周期相關(guān)的方法;其二蚓聘,通過setContentView()把XML解析成View對象腌乡。這里兵分2路,先來看一下setContentView()或粮。

setContentView()方法

這里借用這篇文章的圖來表示setContentView整個流程
https://blog.csdn.net/Rayht/article/details/80782697

setContentView.png

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
}

從這里進去會進去到Activity.setContentView()导饲,最后會調(diào)用PhoneWindow.setContentView()

##PhoneWindow
@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) {
            //1.初始化DecorView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        //判斷有沒有轉(zhuǎn)場動畫
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            //2.解析傳進來的xml布局捞高,生成一個ContentView
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

這里分為2條線氯材,我們先來看一下初始化mDecor。

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

protected DecorView generateDecor(int featureId) {
        Context context;
        ......
        //DecorView extends FrameLayout
        return new DecorView(context, featureId, this, getAttributes());
    }

mDecor就是DecorView對象硝岗,它是PhoneWindow的頂層View氢哮,查看DecorView源碼可以看出DecorView 是一個FrameLayout,但是源碼中并沒有展示出它是一個怎樣的布局型檀,因為它是在注釋2mContentParent = generateLayout(mDecor)中添加的冗尤,下面就來一起看一下。

##PhoneWindow. generateLayout()
protected ViewGroup generateLayout(DecorView decor) {
          // Inflate the window decor.

        int layoutResource; //是一個布局文件id
        int features = getLocalFeatures();
        // 根據(jù)不同的主題將對應(yīng)的布局文件id賦值給layoutResource
        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) {
            ....
            }
            ....          
         else {
            layoutResource = R.layout.screen_simple;
        }

        mDecor.startChanging();
        //通過LayoutInflater加載解析layoutResource布局文件
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        .......
}

這里展示的是generateLayout的上半部分胀溺,先根據(jù)不同的主題生成不同的布局文件裂七,然后解析layoutResource布局文件。

onResourcesLoaded()

##DecorView. onResourcesLoaded()
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        .....

        mDecorCaptionView = createDecorCaptionView(inflater);
        //1.通過XmlResourceParser解析XML布局文件仓坞,得到一個View對象
        //這里的root就是DecorView中添加的layoutResource 
        final View root = inflater.inflate(layoutResource, null);
        //2.把layoutResource布局文件添加到DecorView中
        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;
        initializeElevation();
    }

現(xiàn)在我們知道了背零,generateLayout其實是給DecorView添加一個布局文件,下面就來看一下DecorView到底是怎樣的布局无埃?

上面說過會根據(jù)不同的主題選擇不同的layoutResource徙瓶,這里我們看一下最常用的layoutResource = R.layout.screen_simple

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <!-- ActionBar    -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以看出這是一個垂直的LinearLayout嫉称,上面是一個ActionBar侦镇,下面是一個id為content的FrameLayout,先給大家透漏一下织阅,這個FrameLayout就是用于裝載平時我們寫的Activity中的xml布局壳繁。

generateLayout下半部分

##PhoneWindow. generateLayout()
protected ViewGroup generateLayout(DecorView decor) {
        int layoutResource; //是一個布局文件id
        ......
        //往DecorView中添加layoutResource布局文件
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        //查找DecorView中id為ID_ANDROID_CONTENT的View
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
        .......
      return contentParent;
}

這里就更簡單了,通過findViewById找到之前布局中id為content的view,最后返回到這個contentParent就是剛才的FrameLayout闹炉。

我們再回到最開始的setContentView()方法

##PhoneWindow
@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) {
            //1.初始化DecorView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        //判斷有沒有轉(zhuǎn)場動畫
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            //2.解析傳進來的xml布局伍派,生成一個ContentView
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

上面我們講了installDecor()實際上就是創(chuàng)建并初始化DecorView對象,也就是完成了mContentParent的初始化剩胁,接下來看一下注釋2诉植,這里是解析Activity中傳進來的布局layoutResID,其中parent是mContentParent昵观,也就是之前說過的那個FrameLayout晾腔,這樣就把Activity中的布局添加到DecorView中了。

小結(jié)一下:

setContentView()流程如下:
1啊犬、在PhoneWindow中創(chuàng)建頂層的DecorView灼擂;
2、在DecorView中會根據(jù)主題的不同加載一個不同的布局觉至;
3剔应、把Activity中的布局解析并添加到DecorView的FrameLayout中

這是我畫的一幅圖,按照這個去看源碼會比較清晰语御。

setContentView().png

到這里算是把setContentView流程分析完了峻贮,但把我們僅僅是把自己的Layout添加到DecorView中,但如何顯示到屏幕上還沒看呢应闯。

布局文件中的UI是如何顯示的呢纤控?

其實在文章開頭說過了,在ActivityThread的生命周期調(diào)度中完成的碉纺。剛才已經(jīng)看過了onCreate()船万,后面還會調(diào)用onStart()、onResume()骨田,而最終UI的顯示就是在onResume()中完成的耿导,也就是說我們平常接觸最多的measurelayout态贤、draw3個方法都在onResume中完成的舱呻。而onResume()是在ActivityThread的handleResumeActivity()中調(diào)用的。

 public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
    unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        // 1.會調(diào)用Activity的onResume
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        
        final Activity a = r.activity;

      
        final int forwardBit = isForward
                ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

        // If the window hasn't yet been added to the window manager,
        // and this guy didn't finish itself or start another activity,
        // then go ahead and add the window.
        boolean willBeVisible = !a.mStartedActivity;
        if (!willBeVisible) {
            try {
                willBeVisible = ActivityTaskManager.getService().willActivityBeVisible(
                        a.getActivityToken());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            //獲取WindowManager對象
            ViewManager wm = a.getWindowManager();
            //初始化窗口布局屬性
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                   //2.WindowManager添加DecorView
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }
            ......
}

這里最關(guān)鍵的地方是注釋1和注釋2抵卫,這里先看一下注釋2狮荔,wm是一個接口ViewManager對象,而wm是通過Activity的getWindowManager()獲取的介粘,最后你會發(fā)現(xiàn)wm是在WindowManagerImpl中初始化的殖氏,

##WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
##WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    ....
     ViewRootImpl root;
     View panelParentView = null;
      //創(chuàng)建ViewRootImpl對象,挺重要的一個類姻采,后面會解釋
     root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

      
            try {
                //1.關(guān)鍵調(diào)用
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
}

這里最關(guān)鍵的是調(diào)用了setView()方法雅采,這個方法內(nèi)又調(diào)用了requestLayout()

##ViewRootImpl
public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            //檢查是否在UI線程
            checkThread();
            mLayoutRequested = true;
            
            scheduleTraversals();
        }
    }

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //callback與Choreographer交互,會在下一幀被渲染時觸發(fā)
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

這里的 mChoreographer實際上是Choreographer對象,Choreographer是在屏幕刷新機制中接收顯示系統(tǒng)的VSync信號婚瓜,postFrameCallback設(shè)置自己的callback與Choreographer交互宝鼓,你設(shè)置的callCack會在下一個frame被渲染時觸發(fā)。這里簡單了解下即可巴刻。這里重點關(guān)注下mTraversalRunnable對象愚铡。

##ViewRootImpl
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

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

traversal是遍歷的意思,也就是說后面會做遍歷操作胡陪,至于為什么沥寥,接著往下看。

##ViewRootImpl
 void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
            //關(guān)鍵代碼
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

performTraversals()
這段代碼非常長柠座,但是核心的地方就是下面注釋的3處邑雅。

##ViewRootImpl
private void performTraversals() {
     int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
     int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);      
     ......

if (mFirst) { 
        ..... 
        // host為DecorView 
        // 調(diào)用DecorVIew 的 dispatchAttachedToWindow,并且把 mAttachInfo 給子view 妈经,view.post就是利用了這個原理
        host.dispatchAttachedToWindow(mAttachInfo, 0); 
        ..... 
    }  
     // Ask host how big it wants to be
     //1.執(zhí)行測量 
     performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
     ......
     //2.執(zhí)行布局
     performLayout(lp, mWidth, mHeight);
    ......
     //3.執(zhí)行繪制
     performDraw();
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
              //調(diào)用View的measure()
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;

        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
       //調(diào)用View的layout()
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}

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

 public void draw(Canvas canvas) {
      ......
      //調(diào)用View的onDraw()
      onDraw(canvas);
      ......
}
  • performMeasure():從根節(jié)點向下遍歷View樹淮野,完成所有ViewGroup和View的測量工作,計算出所有ViewGroup和View顯示出來需要的高度和寬度吹泡;

  • performLayout():從根節(jié)點向下遍歷View樹骤星,完成所有ViewGroup和View的布局計算工作,根據(jù)測量出來的寬高及自身屬性荞胡,計算出所有ViewGroup和View顯示在屏幕上的區(qū)域妈踊;

  • performDraw():從根節(jié)點向下遍歷View樹,完成所有ViewGroup和View的繪制工作泪漂,根據(jù)布局過程計算出的顯示區(qū)域,將所有View的當(dāng)前需顯示的內(nèi)容畫到屏幕上歪泳。

現(xiàn)在明白了為什么前面那個方法的名字是遍歷了吧萝勤,因為最后是要完成以DecorView為根節(jié)點的view樹的遍歷。

大家對照我畫的這幅圖去看源碼呐伞,會比較好理解敌卓。


View繪制流程.png

關(guān)于View繪制流程真的很長,代碼量也大伶氢,但是我們只需要關(guān)注核心流程就可以了趟径。最后做一個總結(jié):

  • 1、在onCreate 方法中通過setContentView將XML布局解析成java對象癣防,并添加到PhoneWindow的DecorView中蜗巧;
  • 2、在onResume中將DecorView添加到WindowManagerImpl中蕾盯,然后通過ViewRootImpl來執(zhí)行View的繪制流程幕屹;
  • 3、在ViewRootImplperformTraversals()方法中分別調(diào)用performMeasureperformLayout望拖、 performDraw來完成測量渺尘、布局、繪制说敏;
  • 4鸥跟、performMeasureperformLayout 盔沫、performDraw這幾個方法最終會調(diào)用measure()锌雀、 layout()draw()來完成最終的繪制迅诬。

參考

Android 自定義View之View的繪制流程(一)
【朝花夕拾】Android自定義View篇之(一)View繪制流程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腋逆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子侈贷,更是在濱河造成了極大的恐慌惩歉,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俏蛮,死亡現(xiàn)場離奇詭異撑蚌,居然都是意外死亡,警方通過查閱死者的電腦和手機搏屑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門争涌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人辣恋,你說我怎么就攤上這事亮垫。” “怎么了伟骨?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵饮潦,是天一觀的道長。 經(jīng)常有香客問我携狭,道長继蜡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任逛腿,我火速辦了婚禮稀并,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘单默。我一直安慰自己碘举,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布雕凹。 她就那樣靜靜地躺著殴俱,像睡著了一般政冻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上线欲,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天明场,我揣著相機與錄音,去河邊找鬼李丰。 笑死苦锨,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的趴泌。 我是一名探鬼主播舟舒,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嗜憔!你這毒婦竟也來了秃励?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吉捶,失蹤者是張志新(化名)和其女友劉穎夺鲜,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呐舔,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡币励,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了珊拼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片食呻。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖澎现,靈堂內(nèi)的尸體忽然破棺而出仅胞,到底是詐尸還是另有隱情,我是刑警寧澤昔头,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布饼问,位于F島的核電站,受9級特大地震影響揭斧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜峻堰,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一讹开、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捐名,春花似錦旦万、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赏半。三九已至,卻和暖如春淆两,著一層夾襖步出監(jiān)牢的瞬間断箫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工秋冰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留仲义,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓剑勾,卻偏偏與公主長得像埃撵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子虽另,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355