【背上Jetpack之ViewModel】即使您不使用MVVM也要了解ViewModel ——ViewModel 的職能邊界

系列文章

【背上Jetpack】Jetpack 主要組件的依賴及傳遞關(guān)系

【背上Jetpack】AdroidX下使用Activity和Fragment的變化

【背上Jetpack之Fragment】你真的會用Fragment嗎防泵?Fragment常見問題以及androidx下Fragment的使用新姿勢

【背上Jetpack之Fragment】從源碼角度看 Fragment 生命周期 AndroidX Fragment1.2.2源碼分析

【背上Jetpack之OnBackPressedDispatcher】Fragment 返回棧預(yù)備篇

【背上Jetpack之Fragment】從源碼的角度看Fragment 返回棧 附多返回棧demo

【背上Jetpack】絕不丟失的狀態(tài) androidx SaveState ViewModel-SaveState 分析

目錄

前言

Android 開發(fā)時友雳,我們使用 activity 和 fragment 作為視圖控制器虑啤, 可能還會使用有一些類可以存儲和提供 UI 數(shù)據(jù)(例如MVP中的 Presenter

但是 當(dāng)配置更改時(如旋轉(zhuǎn)屏幕)洗鸵,activity 會重建,但對于 UI 數(shù)據(jù)的持有者呢椿息?

  • 開發(fā)者需要重新保存相關(guān)的信息并傳遞給重建的 activity 涛菠,否則開發(fā)者必須再次獲取數(shù)據(jù)(通過網(wǎng)絡(luò)請求或本地數(shù)據(jù)庫)
  • 由于 UI 數(shù)據(jù)的持有者的生命周期可能比 activity 長铺然,因此開發(fā)者還需要避免出現(xiàn)內(nèi)存泄漏的問題

如何解決上述問題?ViewModel

本文重點介紹 ViewModel 的職責(zé)(what)以及重點功能的實現(xiàn)原理(how)且轨,即使您不使用 Jetpack MVVM 架構(gòu)浮声,也要了解一下 ViewModel

ViewModel 的原理部分要求您了解 activity 的啟動流程,這部分內(nèi)容網(wǎng)上文章很多旋奢,本文不再贅述

ViewModel 的職責(zé)

我先上個 視頻 泳挥,這個小姐姐表述的比文字更形象

ViewModel 主要用于存儲 UI 數(shù)據(jù)以及生命周期感知的數(shù)據(jù)

image

圖片來自 Android Architecture Components: ViewModel

ViewModel生命周期

ViewModel 的生命周期 ,圖片來自 官方文檔

作為數(shù)據(jù)持有者

ViewModel 能夠?qū)崟r進(jìn)行配置更改黄绩。 這意味著即使在手機旋轉(zhuǎn)后銷毀并重新創(chuàng)建 activity 之后羡洁,您仍然擁有相同的 ViewModel 和相同的數(shù)據(jù)。 因此:

  • 您無需擔(dān)心 UI 數(shù)據(jù)持有者的生命周期爽丹。 ViewModel 將由工廠自動創(chuàng)建筑煮,您無需自行創(chuàng)建和銷毀
  • 數(shù)據(jù)將始終更新,旋轉(zhuǎn)手機后粤蝎,您將獲得與以前相同的數(shù)據(jù)真仲。 因此,您無需手動將數(shù)據(jù)傳遞給新的 activity 實例或再次調(diào)用網(wǎng)絡(luò)或數(shù)據(jù)庫來獲取數(shù)據(jù)初澎。

Fragment 間共享數(shù)據(jù)

一個 activity 中的兩個或更多 fragment 需要相互通信是很常見的秸应。例如您有一個片段,用戶在其中從列表中選擇一個 item碑宴,另一個片段顯示了所選 item 的內(nèi)容软啼。 傳統(tǒng)做法兩個 fragment 都需要定義一些接口,并且宿主 activity 必須將兩者綁定在一起延柠。 此外祸挪,兩個 fragment 都必須處理另一個 fragment 尚未創(chuàng)建或不可見的情況。

可以通過使用 ViewModel 對象解決此問題贞间。 這些 fragment 可以使用 activity 范圍內(nèi)共享一個 ViewModel 來處理此通信贿条,如以下示例代碼所示:

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

    public void select(Item item) {
        selected.setValue(item);
    }

    public LiveData<Item> getSelected() {
        return selected;
    }
}


public class MasterFragment extends Fragment {
    private SharedViewModel model;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        model.getSelected().observe(getViewLifecycleOwner(), { item ->
           // Update the UI.
        });
    }
}

由于 兩個 fragment 使用的都是 activity 范圍的 ViewModelViewModelProvider 構(gòu)造器傳入的 activity ),因此它們獲得了相同的 ViewModel 實例增热,自然其持有的數(shù)據(jù)也是相同的整以,這也 保證了數(shù)據(jù)的一致性

這種方法具有以下優(yōu)點:

  • 宿主 activity 無需執(zhí)行任何操作,也無需了解此通信峻仇。

  • SharedViewModel 外公黑,fragment 不需要彼此了解。 如果其中一個 fragment 消失了,則另一個繼續(xù)照常工作帆调。

  • 每個 fragment 都有其自己的生命周期奠骄,并且不受另一個 fragment 的生命周期影響。 如果一個 fragment 替換了另一個 fragment番刊,則 UI 可以繼續(xù)正常工作而不會出現(xiàn)任何問題。

代替 Loader

CursorLoader 這樣的 Loader 類經(jīng)常用于使應(yīng)用程序 UI 中的數(shù)據(jù)與數(shù)據(jù)庫保持同步影锈。您可以使用 ViewModel 和其他一些類來替換 Loader芹务。 使用 ViewModel 可將視圖控制器與數(shù)據(jù)加載操作分開,這意味著您在類之間的強引用較少鸭廷。

在使用 Loader 的一種常見方法中枣抱,應(yīng)用程序可能會使用 CursorLoader 來觀察數(shù)據(jù)庫的內(nèi)容。 當(dāng)數(shù)據(jù)庫中的值更改時辆床,加載程序會自動觸發(fā)數(shù)據(jù)的重新加載并更新 UI

image

圖片來自 官方文檔

ViewModelRoomLiveData 一起使用以替換 Loader佳晶。 ViewModel 確保數(shù)據(jù)在設(shè)備配置更改后仍然存在。 當(dāng)數(shù)據(jù)庫發(fā)生更改時讼载,Room 會通知 LiveData 轿秧,然后 LiveData 會使用修改后的數(shù)據(jù)更新 UI

image

圖片來自 官方文檔

總結(jié)

  • ViewModel 可作為 UI 數(shù)據(jù)的持有者,在 activity/fragment 重建時 ViewModel 中的數(shù)據(jù)不受影響咨堤,同時可以避免內(nèi)存泄漏
  • 可以通過 ViewModel 來進(jìn)行 activity 和 fragment 菇篡,fragment 和 fragment 之間的通信,無需關(guān)心通信的對方是否存在一喘,使用 application 范圍的 ViewModel 可以進(jìn)行全局通信
  • 可以代替 Loader

ViewModel 源碼分析

分析源碼時我們可以不計較細(xì)枝末節(jié)驱还,只分析主要的邏輯即可。因此我們來思考幾個問題凸克,并從源碼中尋找答案

  • 如何做到 activity 重建后 ViewModel 仍然存在议蟆?

  • 如何做到 fragment 重建后 ViewModel 仍然存在?

  • 如何控制作用域萎战?(即保證相同作用域獲取的 ViewModel 實例相同)

  • 如何避免內(nèi)存泄漏咐容?

維持我們一貫的風(fēng)格,我們先來大膽地猜一猜

對于問題1 :activity 有著 saveInstanceState 機制撞鹉,因此可能通過該機制來處理(事實證明不是

對于問題2:可能 fragment 通過 宿主 activity 或 父 fragment 的幫助來確保 ViewModel 實例在重建后仍然存在

對于問題3:實現(xiàn)一個類似單例的效果疟丙,相同作用域獲取的對象是相同的

對于問題4:避免 ViewModel 持有 view 或 context 的引用

首先我們要先了解一下 ViewModel 的結(jié)構(gòu)

  • ViewModel:抽象類,主要有 clear 方法鸟雏,它是 final 級享郊,不可修改,clear 方法中包含 onClear 鉤子孝鹊,開發(fā)者可重寫 onClear 方法來自定義數(shù)據(jù)的清空

  • ViewModelStore:內(nèi)部維護(hù)一個 HashMap 以管理 ViewModel

  • ViewModelStoreOwner:接口炊琉,ViewModelStore 的作用域,實現(xiàn)類為 ComponentActivityFragment,此外還有 FragmentActivity.HostCallbacks

  • ViewModelProvider:用于創(chuàng)建 ViewModel苔咪,其構(gòu)造方法有兩個參數(shù)锰悼,第一個參數(shù)傳入 ViewModelStoreOwner ,確定了 ViewModelStore 的作用域团赏,第二個參數(shù)為 ViewModelProvider.Factory箕般,用于初始化 ViewModel 對象,默認(rèn)為 getDefaultViewModelProviderFactory() 方法獲取的 factory

簡單來說 ViewModelStoreOwner 持有 ViewModelStore 持有 ViewModel

image

1. 如何做到 activity 重建后 ViewModel 仍然存在舔清?

【背上Jetpack】絕不丟失的狀態(tài) androidx SaveState ViewModel-SaveState 分析 中我們提到了 androidx.core.app.ComponentActivity 的引入并探討了其作為中間層的作用

image

我們已經(jīng)講過 SavedStateRegistryOwnerOnBackPressedDispatcherOwner 這兩種角色丝里,而今天我們來聊一下

ViewModelStoreOwnerHasDefaultViewModelProviderFactory 。其中前者代表著 ViewModelStore 的作用域体谒,后者來標(biāo)記 ViewModelStoreOwner 擁有默認(rèn)的 ViewModelProvider.Factory

那么 ViewModel 的邏輯肯定就在該類了

ComponentActivity 實現(xiàn)了 ViewModelStoreOwner 接口杯聚,意味著需要重寫 getViewModelStore() 方法,該方法為 ComponentActivitymViewModelStore 變量賦值抒痒。activity 重建后 ViewModel 仍然存在幌绍,只要保證 activity 重建后 mViewModelStore 變量值不變即可

順著這個思路,我們來看一下 getViewModelStore() 的實現(xiàn)

public ViewModelStore getViewModelStore() {
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            //核心故响,在該位置重置 mViewModelStore
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}

mViewModelStore 的值由 getLastNonConfigurationInstance() 返回的 NonConfigurationInstances 對象中的 viewModelStore 賦值傀广,如果此時還為空才去 new ViewModelStore 對象。因此我們只需找到

getLastNonConfigurationInstance 中的 NonConfigurationInstances 在哪里保存的即可

getLastNonConfigurationInstance 為平臺 activity 中的方法被去,返回 mLastNonConfigurationInstances.activity

public Object getLastNonConfigurationInstance() {
    return mLastNonConfigurationInstances != null
            ? mLastNonConfigurationInstances.activity : null;
}

那么我們看一下 mLastNonConfigurationInstances 的賦值位置

//省略其他參數(shù)
final void attach(NonConfigurationInstances lastNonConfigurationInstances){
    mLastNonConfigurationInstances = lastNonConfigurationInstances;
    //...
}

了解過 activity 的啟動流程的小伙伴肯定知道主儡,這個 attach 方法是 ActivityThread 中的 performLaunchActivity 調(diào)用的

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    //省略其他參數(shù)
    activity.attach(r.lastNonConfigurationInstances);
    r.lastNonConfigurationInstances = null;
    //...
}

深入追蹤源碼我們整理一下調(diào)用流程

image

由于 ActivityThread 中的 ActivityClientRecord 不受 activity 重建的影響,所以 activity 重建時 mLastNonConfigurationInstances 能夠得到上一次的值惨缆,使得 ViewModelStore 值不變 糜值,問題1就解決了

2. 如何做到 fragment 重建后 ViewModel 仍然存在?

對于問題2坯墨,有了上面的思路我們可以認(rèn)定 fragment 重建后其內(nèi)部的 getViewModelStore() 方法返回的對象是相同的寂汇。

// Fragment.java
public ViewModelStore getViewModelStore() {
    return mFragmentManager.getViewModelStore(this);
}

可以看到 getViewModelStore() 內(nèi)部調(diào)用的是 mFragmentManager(普通fragment 對應(yīng) activity 中的 FragmentManager,子 fragment 則對應(yīng)父 fragment 的 childFragmentManager)的 getViewModelStore() 方法

// FragmentManager.java
private FragmentManagerViewModel mNonConfig;

ViewModelStore getViewModelStore(@NonNull Fragment f) {
    return mNonConfig.getViewModelStore(f);
}

而 FragmentManager 中的 getViewModelStore 使用的是 mNonConfig 捣染,mNonConfig 竟然是個 ViewModel骄瓣!

// FragmentManagerViewModel.java
private final HashMap<String, FragmentManagerViewModel> mChildNonConfigs = new HashMap<>();
private final HashMap<String, ViewModelStore> mViewModelStores = new HashMap<>();

FragmentManagerViewModel 管理著內(nèi)部的 ViewModelStore 和 child 的 FragmentManagerViewModel 。因此保證 mNonConfig 值不變即能確保 fragment 中的 getViewModelStore() 不變耍攘。那么看看 mNonConfig 賦值的位置

// FragmentManager.java
void attachController(@NonNull FragmentHostCallback<?> host, @NonNull FragmentContainer container, @Nullable final Fragment parent) {
    //...
    if (parent != null) {
        // 嵌套 fragment 的情況榕栏,有父 fragment
        mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
    } else if (host instanceof ViewModelStoreOwner) {
        // host 是 FragmentActivity.HostCallbacks
        ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
        mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
    } else {
        mNonConfig = new FragmentManagerViewModel(false);
    }
}


// FragmentManagerViewModel.java
static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {
    ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,
            FACTORY);
    return viewModelProvider.get(FragmentManagerViewModel.class);
}

我們先看 fragment 的直接宿主是 activity (即沒有嵌套)的情況,mNonConfig 由FragmentManagerViewModel.getInstance(viewModelStore) 賦值蕾各,而 getInstance 中使用的是 ViewModelProvider 獲取 ViewModel 扒磁,根據(jù)我們上面的分析,只要保證作用域(viewModelStore)相同式曲,即可獲取相同的 ViewModel 實例妨托,因此我們需要看一下 host 的 getViewModelStore 方法缸榛。經(jīng)過一番尋找,host 是 FragmentActivity.HostCallbacks

// FragmentActivity.java 內(nèi)部類
class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements ViewModelStoreOwner, OnBackPressedDispatcherOwner {
    public ViewModelStore getViewModelStore() {
        // 宿主 activity 的 getViewModelStore
        return FragmentActivity.this.getViewModelStore();
    }
}

host 的 getViewModelStore 方法返回的是宿主 activity 的 getViewModelStore() 兰伤,而 activity 重建后其內(nèi)部的 mViewModelStore 是不變的内颗,因此即使 activity 重建,其內(nèi)部的 FragmentManager 對象變化敦腔,但 FragmentManager 內(nèi)部的 FragmentManagerViewModel 的實例(mNonConfig)不變均澳,mNonConfig.getViewModelStore 不變,fragment 的 getViewModelStore() 亦不變符衔,fragment 重建后其內(nèi)部的 ViewModel 仍然存在

對于嵌套 fragment 负懦,mNonConfig 通過 parent.mFragmentManager.getChildNonConfig(parent) 獲取

// FragmentManager.java
private FragmentManagerViewModel getChildNonConfig(@NonNull Fragment f) {
    return mNonConfig.getChildNonConfig(f);
}

上文提到 FragmentManagerViewModel 管理著 mChildNonConfigs Map,因此子 fragment 重置后其內(nèi)部的 mNonConfig 對象也是相同的

至此問題 2 就解決了

3. 如何控制作用域柏腻?

對于問題3,我們知道 ViewModelStoreOwner 代表著作用域系吭,其內(nèi)部唯一的方法返回 ViewModelStore 對象五嫂,也即不同的作用域?qū)?yīng)不同的 ViewModelStore ,而 ViewModelStore 內(nèi)部維護(hù)著 ViewModel 的 HashMap 肯尺,因此只要保證相同作用域的 ViewModelStore 對象相同就能保證相同作用域獲取到相同的 ViewModel 對象沃缘,而問題1我們已經(jīng)解釋了重建時如何保證 ViewModelStore 對象不變。

因此問題3也解決了则吟。

4. 如何避免內(nèi)存泄漏槐臀?

對于問題4,由于 ViewModel 的設(shè)計氓仲,使得 activity/fragment 依賴它水慨,而 ViewModel 不依賴視圖控制器。因此只要不讓 ViewModel 持有 context 或 view 的引用敬扛,就不會造成內(nèi)存泄漏

總結(jié)

簡單的總結(jié)一下:

  • activity 重建后 mViewModelStore 通過 ActivityThread 的一系列方法能夠保持不變晰洒,從而當(dāng) activity 重建時 ViewModel 中的數(shù)據(jù)不受影響

  • 通過宿主 activity 范圍內(nèi)共享的 FragmentManagerViewModel 來存儲 fragment 的 ViewModelStore 和子fragment 的 FragmentManagerViewModel ,而 activity 重建后 FragmentManagerViewModel 中的數(shù)據(jù)不受影響啥箭,因此 fragment 內(nèi)部的 ViewModel 的數(shù)據(jù)也不受影響

  • 通過同一 ViewModelStoreOwner 獲取的 ViewModelStore 相同谍珊,從而保證同一作用域通過 ViewModelProvider 獲取的ViewModel 對象是相同的

  • 通過單向依賴(視圖控制器持有 ViewModel )來解決內(nèi)存泄漏的問題

ViewModel 和 onSaveInstanceState

ViewModelonSaveInstanceState 的功能有些類似,但它們也有很多差異

image

從存儲位置上來說急侥,ViewModel 是在內(nèi)存中砌滞,因此其讀寫速度更快,但當(dāng)進(jìn)程被系統(tǒng)殺死后坏怪,ViewModel 中的數(shù)據(jù)也不存在了贝润。從數(shù)據(jù)存儲的類型上來看,ViewModel 適合存儲相對較重的數(shù)據(jù)陕悬,例如網(wǎng)絡(luò)請求到的 list 數(shù)據(jù)题暖,而 onSaveInstanceState 適合存儲輕量可序列化的數(shù)據(jù)

那么我們該如何使用呢?可以使用 viewmodel-savedstate 庫,詳情參考 【背上Jetpack】絕不丟失的狀態(tài) androidx SaveState ViewModel-SaveState 分析


關(guān)于我


我是 Fly_with24

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胧卤,一起剝皮案震驚了整個濱河市唯绍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌枝誊,老刑警劉巖况芒,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異叶撒,居然都是意外死亡绝骚,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門祠够,熙熙樓的掌柜王于貴愁眉苦臉地迎上來压汪,“玉大人,你說我怎么就攤上這事古瓤≈蛊剩” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵落君,是天一觀的道長穿香。 經(jīng)常有香客問我,道長绎速,這世上最難降的妖魔是什么皮获? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮纹冤,結(jié)果婚禮上洒宝,老公的妹妹穿的比我還像新娘。我一直安慰自己赵哲,他們只是感情好待德,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著枫夺,像睡著了一般将宪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上橡庞,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天较坛,我揣著相機與錄音,去河邊找鬼扒最。 笑死丑勤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吧趣。 我是一名探鬼主播法竞,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼耙厚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了岔霸?” 一聲冷哼從身側(cè)響起薛躬,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呆细,沒想到半個月后型宝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡絮爷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年趴酣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坑夯。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡岖寞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出柜蜈,到底是詐尸還是另有隱情慎璧,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布跨释,位于F島的核電站,受9級特大地震影響厌处,放射性物質(zhì)發(fā)生泄漏鳖谈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一阔涉、第九天 我趴在偏房一處隱蔽的房頂上張望缆娃。 院中可真熱鬧,春花似錦瑰排、人聲如沸贯要。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽崇渗。三九已至,卻和暖如春京郑,著一層夾襖步出監(jiān)牢的瞬間宅广,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工些举, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留跟狱,地道東北人。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓户魏,卻偏偏與公主長得像驶臊,于是被迫代替她去往敵國和親挪挤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容