【Android】View繪制流程

1 概述

圖1.1 Activity繪制過程

對上圖做出簡單解釋:DecorView是一個應用窗口的根容器,它本質上是一個FrameLayout。DecorView有唯一一個子View,它是一個垂直LinearLayout,包含兩個子元素,一個是TitleView(ActionBar的容器),另一個是ContentView(窗口內容的容器)。關于ContentView,它是一個FrameLayout(android.R.id.content),我們平常用的setContentView就是設置它的子View。上圖還表達了每個Activity都與一個Window(具體來說是PhoneWindow)相關聯,用戶界面則由Window所承載。

2 view繪制流程

2.1 view繪制的三個階段

  • measure: 判斷是否需要重新計算View的大小,需要的話則計算;
  • layout: 判斷是否需要重新計算View的位置,需要的話則計算;
  • draw: 判斷是否需要重新繪制View,需要的話則重繪制。

這三個子階段可以用下圖來描述:


圖2.1 View的繪制流程

2.2 measure階段

此階段的目的是計算出控件樹中各個控件顯示所需要的尺寸。
measure方法是為了確定View的大小,父容器會提供寬度和高度參數的約束信息。該方法是一個final類型的方法,意味著子類不能重寫它。真正的測量工作是在View的onMeasure方法中進行,因此關注onMeasure方法的實現即可。

2.2.1測量參數

  • MeasureSpec.UNSPECIFIED:父容器不對View有任何限制,要多大給多大。這種情況一般用于系統內部,表示一種測量的狀態。
  • MeasureSpec.EXACTLY:父容器已經為子容器設置了尺寸,子容器應當服從這些邊界,不論子容器想要多大的空間。它對應于LayoutParams中的match_parent和具體數值這兩種模式
  • MeasureSpec.AT_MOST:父容器指定了一個可用大小,View的大小不能超過這個值。它對應于LayoutParams中的wrap_content。

父View的measure的過程會先測量子View,等子View測量結果出來后,再來測量自己,上面的measureChildWithMargins就是用來測量某個子View的,我們來分析是怎樣測量的,具體看注釋:

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
 
// 子View的LayoutParams,你在xml的layout_width和layout_height,
// layout_xxx的值最后都會封裝到這個個LayoutParams。
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  
 
//根據父View的測量規格和父View自己的Padding,
//還有子View的Margin和已經用掉的空間大小(widthUsed),就能算出子View的MeasureSpec,具體計算過程看getChildMeasureSpec方法。
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); 
 
//通過父View的MeasureSpec和子View的自己LayoutParams的計算,算出子View的MeasureSpec,然后父容器傳遞給子容器的
// 然后讓子View用這個MeasureSpec(一個測量要求,比如不能超過多大)去測量自己,如果子View是ViewGroup 那還會遞歸往下測量。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 
}
 
// spec參數   表示父View的MeasureSpec
// padding參數    父View的Padding+子View的Margin,父View的大小減去這些邊距,才能精確算出
//               子View的MeasureSpec的size
// childDimension參數  表示該子View內部LayoutParams屬性的值(lp.width或者lp.height)
//                    可以是wrap_content、match_parent、一個精確指(an exactly size), 
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 
    int specMode = MeasureSpec.getMode(spec);  //獲得父View的mode 
    int specSize = MeasureSpec.getSize(spec);  //獲得父View的大小 
 
   //父View的大小-自己的Padding+子View的Margin,得到值才是子View的大小。
    int size = Math.max(0, specSize - padding);  
 
    int resultSize = 0;    //初始化值,最后通過這個兩個值生成子View的MeasureSpec
    int resultMode = 0;    //初始化值,最后通過這個兩個值生成子View的MeasureSpec
 
    switch (specMode) { 
    // Parent has imposed an exact size on us 
    //1、父View是EXACTLY的 ! 
    case MeasureSpec.EXACTLY:  
        //1.1、子View的width或height是個精確值 (an exactly size) 
        if (childDimension >= 0) {           
            resultSize = childDimension;         //size為精確值 
            resultMode = MeasureSpec.EXACTLY;    //mode為 EXACTLY 。 
        }  
        //1.2、子View的width或height為 MATCH_PARENT/FILL_PARENT  
        else if (childDimension == LayoutParams.MATCH_PARENT) { 
            // Child wants to be our size. So be it. 
            resultSize = size;                   //size為父視圖大小 
            resultMode = MeasureSpec.EXACTLY;    //mode為 EXACTLY 。 
        }  
        //1.3、子View的width或height為 WRAP_CONTENT 
        else if (childDimension == LayoutParams.WRAP_CONTENT) { 
            // Child wants to determine its own size. It can't be 
            // bigger than us. 
            resultSize = size;                   //size為父視圖大小 
            resultMode = MeasureSpec.AT_MOST;    //mode為AT_MOST 。 
        } 
        break; 
 
    // Parent has imposed a maximum size on us 
    //2、父View是AT_MOST的 !     
    case MeasureSpec.AT_MOST: 
        //2.1、子View的width或height是個精確值 (an exactly size) 
        if (childDimension >= 0) { 
            // Child wants a specific size... so be it 
            resultSize = childDimension;        //size為精確值 
            resultMode = MeasureSpec.EXACTLY;   //mode為 EXACTLY 。 
        } 
        //2.2、子View的width或height為 MATCH_PARENT/FILL_PARENT 
        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;                  //size為父視圖大小 
            resultMode = MeasureSpec.AT_MOST;   //mode為AT_MOST 
        } 
        //2.3、子View的width或height為 WRAP_CONTENT 
        else if (childDimension == LayoutParams.WRAP_CONTENT) { 
            // Child wants to determine its own size. It can't be 
            // bigger than us. 
            resultSize = size;                  //size為父視圖大小 
            resultMode = MeasureSpec.AT_MOST;   //mode為AT_MOST 
        } 
        break; 
 
    // Parent asked to see how big we want to be 
    //3、父View是UNSPECIFIED的 ! 
    case MeasureSpec.UNSPECIFIED: 
        //3.1、子View的width或height是個精確值 (an exactly size) 
        if (childDimension >= 0) { 
            // Child wants a specific size... let him have it 
            resultSize = childDimension;        //size為精確值 
            resultMode = MeasureSpec.EXACTLY;   //mode為 EXACTLY 
        } 
        //3.2、子View的width或height為 MATCH_PARENT/FILL_PARENT 
        else if (childDimension == LayoutParams.MATCH_PARENT) { 
            // Child wants to be our size... find out how big it should 
            // be 
            resultSize = 0;                        //size為0! ,其值未定 
            resultMode = MeasureSpec.UNSPECIFIED;  //mode為 UNSPECIFIED 
        }  
        //3.3、子View的width或height為 WRAP_CONTENT 
        else if (childDimension == LayoutParams.WRAP_CONTENT) { 
            // Child wants to determine its own size.... find out how 
            // big it should be 
            resultSize = 0;                        //size為0! ,其值未定 
            resultMode = MeasureSpec.UNSPECIFIED;  //mode為 UNSPECIFIED 
        } 
        break; 
    } 
    //根據上面邏輯條件獲取的mode和size構建MeasureSpec對象。 
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 
}

如果父View的MeasureSpec 是EXACTLY,說明父View的大小是確切的,(確切的意思很好理解,如果一個View的MeasureSpec 是EXACTLY,那么它的size 是多大,最后展示到屏幕就一定是那么大)

  • 如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是確切,子View的大小又MATCH_PARENT(充滿整個父View),那么子View的大小肯定是確切的,而且大小值就是父View的size。所以子View的size=父View的size,mode=EXACTLY
  • 如果子View 的layout_xxxx是WRAP_CONTENT,也就是子View的大小是根據自己的content 來決定的,但是子View的畢竟是子View,大小不能超過父View的大小,但是子View的是WRAP_CONTENT,我們還不知道具體子View的大小是多少,要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 調用的時候才去真正測量子View 自己content的大?。ū热鏣extView wrap_content 的時候你要測量TextView content 的大小,也就是字符占用的大小,這個測量就是在child.measure(childWidthMeasureSpec, childHeightMeasureSpec)的時候,才能測出字符的大小,MeasureSpec 的意思就是假設你字符100px,但是MeasureSpec 要求最大的只能50px,這時候就要截掉了)。通過上述描述,子View MeasureSpec mode的應該是AT_MOST,而size 暫定父View的 size,表示的意思就是子View的大小沒有不確切的值,子View的大小最大為父View的大小,不能超過父View的大小(這就是AT_MOST 的意思),然后這個MeasureSpec 做為子View measure方法 的參數,做為子View的大小的約束或者說是要求,有了這個MeasureSpec子View再實現自己的測量。
  • 如果如果子View 的layout_xxxx是確定的值(200dp),那么就更簡單了,不管你父View的mode和size是什么,我都寫死了就是200dp,那么控件最后展示就是就是200dp,不管我的父View有多大,也不管我自己的content 有多大,反正我就是這么大,所以這種情況MeasureSpec 的mode = EXACTLY 大小size=你在layout_xxxx 填的那個值

如果父View的MeasureSpec 是AT_MOST,說明父View的大小是不確定,最大的大小是MeasureSpec 的size值,不能超過這個值。

  • 如果子View 的layout_xxxx是MATCH_PARENT,父View的大小是不確定(只知道最大只能多大),子View的大小MATCH_PARENT(充滿整個父View),那么子View你即使充滿父容器,你的大小也是不確定的,父View自己都確定不了自己的大小,你MATCH_PARENT你的大小肯定也不能確定的,所以子View的mode=AT_MOST,size=父View的size,也就是你在布局雖然寫的是MATCH_PARENT,但是由于你的父容器自己的大小不確定,導致子View的大小也不確定,只知道最大就是父View的大小。
  • 如果子View 的layout_xxxx是WRAP_CONTENT,父View的大小是不確定(只知道最大只能多大),子View又是WRAP_CONTENT,那么在子View的Content沒算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暫定父View的 size。
  • 如果如果子View 的layout_xxxx是確定的值(200dp),同上,寫多少就是多少,改變不了的。

如果父View的MeasureSpec 是UNSPECIFIED(未指定),表示沒有任何束縛和約束,不像AT_MOST表示最大只能多大,也不像EXACTLY表示父View確定的大小,子View可以得到任意想要的大小,不受約束

  • 如果子View 的layout_xxxx是MATCH_PARENT,因為父View的MeasureSpec是UNSPECIFIED,父View自己的大小并沒有任何約束和要求,那么對于子View來說無論是WRAP_CONTENT還是MATCH_PARENT,子View也是沒有任何束縛的,想多大就多大,沒有不能超過多少的要求,一旦沒有任何要求和約束,size的值就沒有任何意義了,所以一般都直接設置成0
  • 同上
  • 如果如果子View 的layout_xxxx是確定的值(200dp),同上,寫多少就是多少,改變不了的(記住,只有設置的確切的值,那么無論怎么測量,大小都是不變的,都是你寫的那個值)

2.2.2 測量過程

View的測量過程主要是在onMeasure()方法中,measure方法是final,所以這個方法不可重寫,如果想自定義View的測量,應該去重寫onMeasure()方法。在onMeasure方法的最后需要調用setMeasuredDimension方法,不然會拋異常

基本思想:父View把自己的MeasureSpec,傳給子View結合子View自己的LayoutParams 算出子View 的MeasureSpec,然后繼續往下傳,傳遞葉子節點,葉子節點沒有子View,根據傳下來的這個MeasureSpec測量自己就好了。所有的孩子測量之后,經過一系列的計算之后通過setMeasuredDimension設置自己的寬高,對于FrameLayout 可能用最大的子View的大小,對于LinearLayout,可能是高度的累加,具體測量的原理去看看源碼。總的來說,父View是等所有的子View測量結束之后,再來測量自己。通過setMeasuredDimension進行測量。


圖2.2 DecorView結構圖

3 layout過程

Layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定后,它在onLayout中會遍歷所有子元素并調用其layout方法,在layout方法中又會調用onLayout方法。
layout方法的大致流程:首先會通過setFrame方法來設定View的四個頂點的位置,即初始化mLeft、mRight、mTop和mBottom,View的四個頂點一旦確定,那么View在父容器中的位置也就確定了;接著會調用onLayout方法,這個方法的用途是父容器確定子元素的位置,因為它的具體實現和具體的布局有關,所以View和ViewGroup都沒有真正實現它,而是由子類重寫。
注意:ViewGroup的layout方法是final類型的,它會調用onLayout方法(ViewGroup中是抽象方法,子類必須重寫),所以子類根據布局需要重寫onLayout方法。View的layout方法是public類型的,但是最終的位置確定是在onLayout方法(View中是public的空方法)中進行的,所以重寫onLayout方法即可。

4 draw過程

draw的繪制過程主要分為6步

  • 繪制背景
  • 如有必要,保存畫布的圖層以準備淡化
  • 對view內容進行繪制
  • 對當前view的所有子view進行繪制
  • 如有必要,繪制淡化邊緣并恢復圖層
  • 對view的滾動條進行繪制

接下來看View的一個方法:

/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

從setWillNotDraw這個方法的注釋可看出,如果一個View不需要繪制任何內容,那么設置這個標記為為true以后,系統會進行相應的優化。默認情況下,View沒有起用這個優化標記位,但是ViewGroup會默認啟用。這個標記位對實際開發的意義是:當我們的自定義控件繼承于ViewGroup并且本身不具備繪制功能時,就可以開始這個標記位便于系統進行優化。如果明確知道一個ViewGroup需要通過onDraw來繪制內容,就需要顯式的關閉WILL_NOT_DRAW 這個標記位。

相關問答

  1. 在Activity啟動的時候獲取某個View的寬高
    答:在onCreate、onStart、onResume中均無法獲取到某個View的正確寬高,因為View的measure過程和Activity的生命周期方法并不是同步執行的,無法保證Activity執行了onCreate、onStart、onResume時某個View已經測量完畢,獲取到的寬高可能為0。可以通過下列四種方法來解決這個問題:
  • Activity重寫onWindowFocusChanged
    onWindowFocusChanged這個方法的含義是:View已經初始化完畢,寬高已經準備好了,所以這時候獲取的寬高是沒問題的。這個方法會被調用多次,當Activity窗口得到焦點和失去焦點的時候都會被調用。
  • view.post(runnable)
    通過post可以講一個runnable投遞到消息隊列的尾部,然后等待Looper調用此runnable的時候,View已經初始化好了。
  • ViewTreeObserver
    使用ViewTreeObserver的眾多回調可以完成這個功能,比如OnGlobalLayoutListener接口:當View樹的狀態發生改變或者View樹內部的View的可見性發生改變時,onGlobalLayout方法將會被回調,這是獲取View寬高的一個好時機。注意該方法會被調用多次。
  • view.measure(int widthMeasureSpec, int heightMeasureSpec)
    通過手動對View進行measure來得到View的寬高,個人不推薦該方法,有局限性,而且容易出錯,就不介紹了。
public class TestActivity extends Activity {

    @BindView(R.id.btn)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        ButterKnife.bind(this);

        textView.post(new Runnable() {
            @Override
            public void run() {
                int width = textView.getMeasuredWidth();
                int height = textView.getMeasuredHeight();
                Log.d("==========", "onCreate view.post  width = " + width + " ; height = " + height);
            }
        });

        ViewTreeObserver viewTreeObserver = textView.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //特別注意:此處不能直接使用viewTreeObserver.removeGlobalOnLayoutListener(this); 會有如下異常:
                //java.lang.IllegalStateException: This ViewTreeObserver is not alive, call getViewTreeObserver() again
                textView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                int width = textView.getMeasuredWidth();
                int height = textView.getMeasuredHeight();
                Log.d("==========", "onCreate viewTreeObserver.addOnGlobalLayoutListener  width = " + width + " ; height = " + height);
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();
        textView.post(new Runnable() {
            @Override
            public void run() {
                int width = textView.getMeasuredWidth();
                int height = textView.getMeasuredHeight();
                Log.d("==========", "onStart view.post  width = " + width + " ; height = " + height);
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        int width = textView.getMeasuredWidth();
        int height = textView.getMeasuredHeight();
        Log.d("==========", "onResume  width = " + width + " ; height = " + height);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus) {
            int width = textView.getMeasuredWidth();
            int height = textView.getMeasuredHeight();
            Log.d("==========", "onWindowFocusChanged  width = " + width + " ; height = " + height);
        }
    }
}

//測試結果:
//onResume  width = 0 ; height = 0
//onCreate viewTreeObserver.addOnGlobalLayoutListener  width = 391 ; height = 57
//onCreate view.post  width = 391 ; height = 57
//onStart view.post  width = 391 ; height = 57
//onWindowFocusChanged  width = 391 ; height = 57

  1. Activity顯示流程
    ViewRoot對應于ViewRootImpl類,它是連接WindowManager和DecorView的紐帶,View的三大流程均是通過viewRoot來完成的。在ActivityThread中,當Activity對象創建完畢后,會將decorView添加到window中,同時會創建ViewRootImpl對象viewRoot,并將viewRoot和decorView建立關聯。
    View的繪制流程從viewRoot的performTraversals方法開始的。
  2. View的測量寬高和顯示的最終寬高區別
    答:測量寬高是在measure過程中確定的,為onMeasure方法中通過setMeasuredDimension設置的值;最終寬高是在layout過程中確定的,寬為mRight - mLeft,高為mBottom - mTop。
    正常情況下,它兩是相等的,某些特殊情況會不一致,比如:
//重寫View的layout方法
public void layout(int l, int t, int r, int b) {
    //最終寬高會比測量寬高大100px
    super.layout(l, t, r + 100, b + 100);
}
  1. MeasureSpec的賦值原理
    答:MeasureSpec代表一個32位的int值,高2位代表SpecMode,低30位代表SpecSize。
    DecorView:MeasureSpec由窗口的尺寸和自身的LayoutParams共同決定;
    普通View:MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定
  2. 當一個TextView的實例調用setText()方法后執行了什么
    答:setText最后會調用requestLayout()和invalidate()。requestLayout()會調用父容器的requestLayout方法,直至頂層View。requestLayout()會調用measure過程和layout過程,invalidate()會調用draw過程。
  3. 幾個常用方法介紹
    requestLayout():調用measure過程和layout過程;
    invalidate():必須要在UI線程調用;View可見的話,會調用onDraw方法;
    postInvalidate():可以在非UI線程調用,通過handler發送一個MSG_INVALIDATE消息,然后在主線程處理消息,執行invalidate()方法。

參考文章

https://www.cnblogs.com/jycboy/p/6219915.html
http://www.lxweimin.com/p/c4412f878508

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容