Android App 啟動時的操作之 ClassLoader 和 Application 初始化

Android App 啟動時的操作之 ClassLoader 和 Application 初始化

公共部分

  1. ActivityManagerService.startProcessLocked()

    當(dāng) app 啟動時, ActivityManagerService.startProcessLocked()app 啟動時啟動進(jìn)程的地方风罩。

  2. ZygoteInit.java 的過程

    startSystemServer() 方法 -> Zygote.forkSystemServer()

  3. RuntimeInit.invokeStaticMain() 反射調(diào)回到 ActivityThread.main(...)

    //反射調(diào)用 ActivityThread.main(...) 
    m = cl.getMethod("main", new Class[] { String[].class });
    

上面做了一個簡述缠导。

Zygotefork 分析趴久,在這里沒有細(xì)說掖鱼。需要的可以參考鏈接:
理解Android進(jìn)程創(chuàng)建流程

Application 和 ClassLoader 部分

1. 補(bǔ)充部分-有關(guān) ClassLoader

app 里以清,系統(tǒng)類加載器有三種: BootClassLoader, PathClassLoader, DexClassLoader典格。

  1. BootClassLoader 是用來加載系統(tǒng) framework 層級的類加載器廓潜, 同時它也是 app 中所有 ClassLoader 的最頂層的 parent

    Android系統(tǒng)啟動時會使用 BootClassLoader 來預(yù)加載常用類

    是個單例母怜。是 ClassLoader 的一個私有內(nèi)部類

  2. PathClassLoader 是用來加載應(yīng)用程序的類余耽, 通常是加載已經(jīng)安裝好的 apk 文件

  3. DexClassLoader 可以加載 dex 文件以及包含 dex 的 apk 文件(安裝好的程序和未安裝的 dex 文件)

    實際上 DexClassLoader 的加載范圍比 PathClassLoader 的加載范圍要大, 它可以加載在 SD 上面的 .jar.apk 文件。

    在一些動態(tài)修復(fù)苹熏,補(bǔ)丁包上面碟贾,是利用 DexClassLoader 去實現(xiàn)的。

現(xiàn)在引出一個問題轨域,Application 的 類加載器 是哪個袱耽??

代碼檢驗一下:

ClassLoader classLoader = baseContext.getClassLoader();
if (classLoader != null) {

    Log.i(TAG, "classLoader is " + classLoader.toString() + " --->from Log");

    while (classLoader.getParent() != null) {
          classLoader = classLoader.getParent();
          Log.i(TAG, "classLoader is " + classLoader.toString() + " --->from Log  in while");
    }
}

打印結(jié)果為:

ThemeLayoutContainer: classLoader is dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.chenzhao.thememaintest-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.chenzhao.thememaintest-1/lib/arm, /data/app/com.example.chenzhao.thememaintest-1/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]] --->from Log
ThemeLayoutContainer: classLoader is java.lang.BootClassLoader@f42ce93 --->from Log  in while

從上面可以看到干发, 加載 application 的類加載器為 dalvik.system.PathClassLoader, 并且它與包名 com.example.chenzhao.thememaintest-1/base.apk 是相關(guān)的朱巨,也就是一對一對應(yīng)的。


為什么會有 parent 這個屬性枉长?

ClassLoaderloadclass() 采用的是 (雙)父親委托模型冀续, 在 BootClassLoaderPathClassLoader 的父親,同時它也是最頂層的一個 classLoader.(腦補(bǔ)樹的圖)

loadclass() 的步驟:加載類 ATest.class

  1. 會先查詢當(dāng)前 ClassLoader(AClassLoader) 是否加載過 ATest必峰,加載過就返回洪唐;

    注: 注意是加載過!!

  2. 如果沒有,查詢 它 (AClassLoader) 的 parent 是否已經(jīng)加載過ATest吼蚁,如果有凭需,就直接返回 parent 加載過的類, 如果沒有,依次向上去尋找它的 parent肝匆;

  3. 如果繼承路線上的 ClassLoader 都沒有加載粒蜈,則會用它 (AClassLoader) 去加載該類 ATest;

  4. 當(dāng)一個類被位于樹根 的 ClassLoader 加載過,那么术唬, 在以后整個系統(tǒng)的生命周期內(nèi)薪伏,這個類永遠(yuǎn)不會被重新加載

    樹根的 ClassLoaderAndroid 系統(tǒng)中是 BootClassLoader


2. 正式開始, 那么粗仓,PathClassLoader 是什么時候初始化的呢嫁怀?

一切都需要從 ActivityThread.main(...) 說起

當(dāng)一個進(jìn)程被創(chuàng)建成功后设捐,會走到 ActivityThread.main(...),

    //ActivityThread.java
    public static void main(String[] args) {
        
        ...
        
        Looper.prepareMainLooper();
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
        ...
        
        Looper.loop();
        
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    

上述代碼,主要的操作在 thread.attach(false);

調(diào)用次序:

調(diào)用時序圖
  1. thread.attach(false);

    這是第一步塘淑,如圖上所示 1. thread.attach(false): 大致代碼如下:

    final IActivityManager mgr = ActivityManager.getService();
    //在這里 mgr 是 AMS(ActivityManagerService)
    try {
       mgr.attachApplication(mAppThread);
    } catch (RemoteException ex) {
       throw ex.rethrowFromSystemServer();
    }
    

    關(guān)鍵代碼:調(diào)用了 mgr.attachApplication(...);

  2. mgr.attachApplication(mAppThread);

    mgrIActivityManager 萝招,在這里是 ActivityManagerService, 即 ActivityManagerService.attachApplication(mAppThread)

    那么需要去 AMS 里面尋找對應(yīng)相關(guān)代碼.

  3. ActivityManagerService.attachApplicationLocked(thread, callingPid);

    在它的里面存捺,會調(diào)用 thread.bindApplication()

    thread 為傳過來的參數(shù)槐沼,是在 ActivityThread 里面的一個成員變量, 它的賦值是在初始化時完成的,代碼如下:

    // ActivityThread 里面
    final ApplicationThread mAppThread = new ApplicationThread();
    

    ApplicationThread 的一個對象捌治,它是 ActivityThread 的內(nèi)部類岗钩,再次回到 ActivityThread, 去找 thread.bindApplication() 這個方法,進(jìn)入第 4 步;

  4. ApplicationThread.bindApplication()

    在這方法里面肖油,最主要的代碼就是 sendMessage(H.BIND_APPLICATION, data);

    發(fā)了一條 message(是主線程的 handler發(fā)送的消息)兼吓, 在 handleMessage(Message) 里面處理了該消息。

    AppBindData data = (AppBindData)msg.obj;
    handleBindApplication(data);
    

    注:在 handleMessage(Message) 這個方法里面可以看到森枪,是主線程的處理位置视搏,里面有很多管理 activity 生命周期的方法和參數(shù), 它便是主線程處理消息的地方县袱。

  1. ActivityThread.handleBindApplication(data)

    這個方法太長了浑娜,太長了!主要代碼看下面的部分:

    Application app = data.info.makeApplication(data.restrictedBackupMode, null);
    mInitialApplication = app;
    ...
    //其實這一步是調(diào)用的 application  的 onCreate() 方法. 如時序圖中的
    mInstrumentation.callApplicationOnCreate(app);
    

    data.info 是什么式散? 在這里是 LoadedApk 這個對象, 這個類是一個比較重要的類筋遭,可以多關(guān)注下.

    注:這里稍微注意一下 mInitialApplication 賦值的操作

  2. LoadedApk.makeApplication(..., null);

    LoadedApk.makeApplication() 里面的代碼會去獲取 ClassLoader, 并且創(chuàng)建 appContext, 再通過 ClassLoaderappContext 去創(chuàng)建 application 對象;

    try {
            java.lang.ClassLoader cl = getClassLoader();
            if (!mPackageName.equals("android")) {
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                        "initializeJavaContextClassLoader");
                initializeJavaContextClassLoader();
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            }
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            appContext.setOuterContext(app);
        } catch (Exception e) {
        ...
    

    終于看到了有關(guān) ClassLoader 的部分。下面具體看 ClassLoader 是如何創(chuàng)建的杂数。 如圖上右上角宛畦。

  3. LoadedApk.getClassLoader()

    首先假設(shè)這個時候 mClassLoader 是空的,然后去看一下 ClassLoader 的創(chuàng)建:(其實這個時候已經(jīng)被創(chuàng)建出來了)

     public ClassLoader getClassLoader() {
        synchronized (this) {
            if (mClassLoader == null) {
                createOrUpdateClassLoaderLocked(null /*addedPaths*/);
            }
            
            return mClassLoader;
        }
     }
    

    注:實際上 LoadedApk.getClassLoader() 在這里并不為 null, ClassLoader 真正創(chuàng)建是在程序的前面,這里暫不做分析.

  4. LoadedApk.createOrUpdateClassLoaderLocked(null)

    在這個方法里面枚驻,LoadedApkmIncludeCode = true;

    會走到:

    if (mClassLoader == null) {
            // Temporarily disable logging of disk reads on the Looper thread
            // as this is early and necessary.
            StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
    
            mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip,
                    mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
                    libraryPermittedPath, mBaseClassLoader);
    
            StrictMode.setThreadPolicy(oldPolicy);
            // Setup the class loader paths for profiling.
            needToSetupJitProfiles = true;
        }
        
    

    那么接下來编曼,看一下 ApplicationLoaders.getDefault().getClassLoader(...)

    注: mIncludeCode 是一個標(biāo)志位,去加載 APK 時是否需要加載它的源碼见芹。mIncludeCode 在一些比較 hack 的方式去獲取其他 apk 的 context 時, truefalse 是有區(qū)別的。

  5. ApplicationLoaders 這個類

    對于 ApplicationLoaders 這個類,是個單例罕邀,它里面維護(hù)了一個 mLoaders, 它是一個 map养距, keystring (可以看做是 包名)诉探,valueClassLoader (類加載器),

    看一下 getClassLoader() 的實現(xiàn):

    public ClassLoader getClassLoader(String zip, ... ) {
        
        ...
        // 首先 檢查 mLoaders map 里 是否有該 loader棍厌, 有即返回肾胯,
        //沒有則創(chuàng)建一個新的 pathClassLoader, 并把新建的 loader 加入 mLoaders
        
        ClassLoader loader = mLoaders.get(zip);
       if (loader != null) {
           return loader;
       }
       
       PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(...);
       ...
       mLoaders.put(zip, pathClassloader);
       
       return pathClassloader;
        
        ... 
    }
    

    到這里為止竖席,一個 app 對應(yīng)的進(jìn)程的 ClassLoader 才被創(chuàng)建成功.

    注:這里應(yīng)該注意到 ApplicationLoaders 里面是可以對應(yīng)保存多個 APK 的 ClassLoader

上述 1~9 是 app 啟動時的一系列動作,7~9 分析的是 ClassLoader 的創(chuàng)建過程敬肚。下面我們看一下 Application 的創(chuàng)建過程毕荐。

從第 6 條開始:

app = mActivityThread.mInstrumentation.newApplication(
                   cl, appClass, appContext);

如圖上右邊 Application 創(chuàng)建時序圖

  1. Instrumentation 是什么?

    Instrumentationandroid 系統(tǒng)里面的一套控制方法或者“鉤子”艳馒,我理解的是憎亚,它可以在正常的生命周期(正常是有系統(tǒng)控制的)之外控制 android 的運行, 拿到一些流程控制的時機(jī)弄慰,然后我們就可以在這些時機(jī)里面寫一些處理我們想要的東西第美。它提供了各種流程控制方法,例如下面的方法:

    callActivityOnCreate()-----> 對應(yīng)著系統(tǒng)的 onCreate();
        
    callActivityOnStart()-----> 對應(yīng)著系統(tǒng)的 onStart();
        
    callActivityOnDestroy()-----> 對應(yīng)著系統(tǒng)的 onDestroy();
    

    所以 當(dāng)我們?nèi)バ陆ㄒ粋€類陆爽, 繼承與 Instrumentation, 重寫一些方法什往,在這些方法里面我們就可以自由的做一些控制的事情,例如墓陈,添加自動測試模塊等恶守。

    Instrumentation.newApplication(cl, appClass, appContext) 的三個參數(shù):

    • cl 根據(jù)上面可知道,它是 ClassLoader

    • appClass 當(dāng)前 app application 的名字:

      appClass = mApplicationInfo.className; 
      
    • appContext: 是當(dāng)前這個 app 對應(yīng)的 context

      作為參數(shù)去創(chuàng)建 application 時贡必, 在 application.attach(context), 把該 context 作為 application 的 context.

  2. Instrumentation.newApplication(...)

    代碼:

    return newApplication(cl.loadClass(className), context);
    

    本質(zhì)上是通過 ClassLoader 根據(jù)路徑名去 loadClass(className)兔港。

    結(jié)合 ClassLoader 的 loadClass() 的邏輯.

    VMClassLoader

  3. Instrumentation.newApplication(clazz, context);

    代碼:

    Application app = (Application)clazz.newInstance();
    app.attach(context);
    return app;
    

    實例話 application 對象,然后賦值給 app, return.

    上述實際上已經(jīng)完成了 application 對象的建立

  1. Application 的 app.attach(context) 是做什么仔拟?

    跳轉(zhuǎn)到 Application 中衫樊,進(jìn)入到代碼里可以看到:

    /* package */ final void attach(Context context) {
        attachBaseContext(context);
        mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
    }
    

    手動調(diào)用了 attachBaseContext(context) 這個方法。

    注:手動跳轉(zhuǎn)到 ContextWrapper.attachBaseContext()

    在 Application 的回調(diào)方法里面利花,我們通常會用到的兩個方法:

    • attachBaseContext(context)

    • onCreate()

    當(dāng)一個 Application 建立起時科侈, 會首先調(diào)用 attachBaseContext() 這個方法。那么什么時候炒事,才會調(diào)用 onCreate() 呢臀栈?

    接著去尋找調(diào)用 onCreate() 的時機(jī)。

  2. Application 調(diào)用 onCreate()

    回到上面的第 5 步: ActivityThread.handleBindApplication(data)

    在其方法里面:

    Application app = data.info.makeApplication(data.restrictedBackupMode, null);
    
    mInitialApplication = app;
    ...
    
     try {
         mInstrumentation.callApplicationOnCreate(app);
     } catch (Exception e) {
     ...
    

    這個時候挠乳,調(diào)用了 mInstrumentation.callApplicationOnCreate(app)权薯;, 調(diào)用位置標(biāo)注為 C 處

    注:makeApplication(...) 調(diào)用時,傳入的第二個參數(shù)為 instrumentation = null, 在 makeApplication(...) 下有關(guān)于 instrumentation 的判斷

  3. Instrumentation.callApplicationOnCreate(app)

    這個方法里面比較簡單:

    // app 為 application 對象
    app.onCreate();
    

    所以在 Application 里面 attachBaseContext() 是早于 onCreate() 的調(diào)用的睡扬。

    如圖所示:調(diào)用 application.attachBaseContext() 的位置是 A, 調(diào)用 application.onCreate() 的位置是 C.

Application 中 getApplicationContext() 與上述兩個方法的關(guān)系

在代碼調(diào)用中盟蚣,

  • attachBaseContext() 里調(diào)用 getApplicationContext() 返回的為 null;

  • onCreate() 里調(diào)用 getApplicationContext() 返回的不為 null;

為什么呢? 圖示如左下角:

看一下 getApplicationContext() 調(diào)用棧:

  1. 會首先在 ContextWrapper.java 里面 getApplicationContext():

    return mBase.getApplicationContext();
    
  2. mBase 是什么卖怜?

    是一個 Context 對象屎开,在 Application.attachBaseContext() 時被賦值的。

    ContextImpl.java 里面去看马靠。

  3. ContextImpl.getApplicationContext()

    代碼如下:

    return (mPackageInfo != null) ?
                mPackageInfo.getApplication() : mMainThread.getApplication();
    

    梳理一下邏輯:

    1. mPackageInfo 什么時候賦值的奄抽?是否為 null

      mPackageInfoLoadedApk 的對象

      mBase 是在 LoadedApk 通過 ContextImpl.createAppContext(mActivityThread, this) 里創(chuàng)建的蔼两,
      mPackageInfo = packageInfo; 出現(xiàn)在 ContextImpl 的構(gòu)造函數(shù)里面,不為 null如孝, 為 this( 為 LoadedApk)

    2. mPackageInfo.getApplication()

      返回的是 return mApplication;

      mApplication 是什么時候賦值的呢宪哩?

  4. 在 mPackageInfo.getApplication() 里 mApplication 的賦值

    在 LoadedApk.getApplication() 里面 對 mApplication 的賦值只有一處, 出現(xiàn)在 LoadedApk.makeApplication() 里面

    app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
    ...
    mApplication = app;
    

    我們已經(jīng)知道,在 mActivityThread.mInstrumentation.newApplication(...) 的時候第晰, Application 已經(jīng)調(diào)用了 attachBaseContext() 所以當(dāng)在 attachBaseContext() 里面調(diào)用 getApplicationContext() 時锁孟,返回的值為 null.

    而 mApplication = app; 之后,才執(zhí)行了 Application.onCreate().

總結(jié):

從上述步驟茁瘦,總結(jié)一下品抽,可以看到:

  1. ClassLoader 的創(chuàng)建要早于這個 Apk 里所有的 Class;

  2. PathClassLoader 是和 APK 相互對應(yīng)的, PathClassLoaderDexPathList 是與 APK 的安裝路徑一一對應(yīng)的;

  3. 一個進(jìn)程是可以對應(yīng)多個 ClassLoader 的甜熔。 在 ApplicationLoader 里面有一個 ArrayMap<String, ClassLoader> mLoaders;

  4. Application 的創(chuàng)建是利用 ClassLoaderloadclass() 實現(xiàn)的

  5. ApplicationattachBaseContext() 是早于 onCreate() 的調(diào)用的

  6. getApplicationContext() 不為 null 是晚于 attachBaseContext()圆恤, 早于 onCreate() 方法的

坑邊閑話

關(guān)于這部分的內(nèi)容,有些枯燥腔稀,查看源碼的過程比較枯燥盆昙,這里面的源碼具體 api 是 26 還是多少,暫時不定焊虏,因為后面你如果自己看源碼可能會發(fā)現(xiàn) api 已經(jīng)修改了淡喜,但大體是這樣一個流程。

希望能在查看這個流程中得到意外的收獲~

當(dāng)然文中不可避免可能會出現(xiàn)錯誤诵闭,我的一些理解可能不足夠準(zhǔn)確炼团,希望不會對看到文章的同學(xué)造成困擾。

參考鏈接:

  1. 以 ClassLoader 為視角看 Android 應(yīng)用的啟動過程

  2. ClassLoader的來源

  3. 理解Android進(jìn)程創(chuàng)建流程

  4. Android 源碼, Zygote.java, ActivityManagerServer.java, Process, ZygoteInit.java, ActivityThread.java, LoadedApk.java, ApplicationLoaders.java

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疏尿,一起剝皮案震驚了整個濱河市瘟芝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌褥琐,老刑警劉巖锌俱,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異敌呈,居然都是意外死亡嚼鹉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門驱富,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人匹舞,你說我怎么就攤上這事褐鸥。” “怎么了赐稽?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵叫榕,是天一觀的道長浑侥。 經(jīng)常有香客問我,道長晰绎,這世上最難降的妖魔是什么寓落? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮荞下,結(jié)果婚禮上伶选,老公的妹妹穿的比我還像新娘。我一直安慰自己尖昏,他們只是感情好仰税,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抽诉,像睡著了一般陨簇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上迹淌,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天河绽,我揣著相機(jī)與錄音,去河邊找鬼唉窃。 笑死耙饰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的句携。 我是一名探鬼主播榔幸,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼矮嫉!你這毒婦竟也來了削咆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤蠢笋,失蹤者是張志新(化名)和其女友劉穎拨齐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昨寞,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡瞻惋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了援岩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歼狼。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖享怀,靈堂內(nèi)的尸體忽然破棺而出羽峰,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布梅屉,位于F島的核電站值纱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏坯汤。R本人自食惡果不足惜虐唠,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望惰聂。 院中可真熱鬧疆偿,春花似錦、人聲如沸庶近。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鼻种。三九已至反番,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間叉钥,已是汗流浹背罢缸。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留投队,地道東北人枫疆。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像敷鸦,于是被迫代替她去往敵國和親息楔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

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