Android gradle打包涉及task源碼解析(六)

文章序號

此篇文章將分析如下7個task。

:app:mergeDebugJniLibFolders
:app:transformNativeLibsWithMergeJniLibsForDebug
:app:processDebugJavaRes NO-SOURCE
:app:transformResourcesWithMergeJavaResForDebug
:app:validateSigningDebug
:app:packageDebug
:app:assembleDebug

mergeDebugJniLibFolders

執行命令:./gradlew mergeDebugJniLibFolders

  • inputs&outputs
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/src/debug/jniLibs
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/src/main/jniLibs
---------------------------------------------------
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/incremental/mergeDebugJniLibFolders
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/jniLibs/debug

輸入文件也就是我們項目的jniLibs目錄。

輸出分為兩種:

增量編譯的目錄(intermediates/incremental/mergeDebugJniLibFolders),暫不分析;

jniLibs目錄(intermediates/jniLibs/debug)。

通過輸入輸出基本能判斷mergeDebugJniLibFolders任務就是把項目的jniLibs目錄合到intermediates/jniLibs/debug下面。

  • 源碼

https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/MergeSourceSetFolders.java

  • 主要代碼邏輯

看到MergeSourceSetFolders這個類是不是很熟悉?在介紹mergeDebugAssetstask時,有分析到這個類,這里就不在具體分析了,實際干的事情類似,就是將相應的目錄下的文件merge到一個指定的目錄下。

transformNativeLibsWithMergeJniLibsForDebug

執行命令:./gradlew transformNativeLibsWithMergeJniLibsForDebug

  • inputs&outputs
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/appcompat-v7-26.1.0.aar/6b443e96f1af9aa241aaa70576c67a57/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.3.aar/f44da5c361a1f52801511229596f72e7/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/butterknife-8.5.1.aar/9d5de52440cb778daab09db33955642f/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/animated-vector-drawable-26.1.0.aar/9c804d63d6f065a8f9945f9ad94fee0e/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-vector-drawable-26.1.0.aar/4e56cc34abf77378e2b8d16ee237c82d/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-annotations/8.5.1/bb67dad90bab7cd77a8f7f1b8442b47e3a2326bc/butterknife-annotations-8.5.1.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-v4-26.1.0.aar/3bf8586900bd31e222ef8b68bfd6e744/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-media-compat-26.1.0.aar/267524a16ca7128dd9cef3c19f394439/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-fragment-26.1.0.aar/77cf518e9868987a283f04cec221fefa/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-core-utils-26.1.0.aar/8634ab1afa6a5a1a947a7bd163aba14f/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-core-ui-26.1.0.aar/8902e2a864b44d47c26fbc80fdafe175/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-compat-26.1.0.aar/3e4c87483eacfb4c962d7380a59a114d/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/com.android.support/support-annotations/26.1.0/814258103cf26a15fcc26ecce35f5b7d24b73f8/support-annotations-26.1.0.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.3/bde0667d7414c16ed62d3cfe993cff7f9d732373/constraint-layout-solver-1.1.3.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/runtime-1.0.0.aar/ed085e7b9476f7a9fef4ffbb323166ba/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/android.arch.lifecycle/common/1.0.0/e414a4cb28434e25c4f6aa71426eb20cf4874ae9/common-1.0.0.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/android.arch.core/common/1.0.0/a2d487452376193fc8c103dd2b9bd5f2b1b44563/common-1.0.0.jar
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/jniLibs/debug/armeabi-v7a/libcommon-jni.so
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/jniLibs/debug/armeabi/libcommon-jni.so
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/jniLibs/debug/GDTActionSDK.min.1.2.0.jar
---------------------------------------------------
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/incremental/debug-mergeJniLibs/zip-cache
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/mergeJniLibs/debug

查看輸入文件,會發現輸入文件類型分為兩種:

1、依賴的庫的jar文件;

2、mergeDebugJniLibFolders任務的輸出。

輸出目錄也很簡單,一個是增量編譯的目錄,一個是真正的merge輸出目錄。

  • 源碼

https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/transforms/MergeJavaResourcesTransform.java

  • 主要代碼邏輯
public void transform(@NonNull TransformInvocation invocation)
            throws IOException, TransformException {
        
        ...
        // 輸入inputs、output執行merge操作
        state = IncrementalFileMerger.merge(ImmutableList.copyOf(inputs), output, state);
        saveMergeState(state);

        cacheUpdates.forEach(Runnable::run);
    }

通過代碼可以很清晰的知道這個任務就是對輸入進行一個merge操作,merge的邏輯在IncrementalFileMerger類里面,這里不在具體分析。MergeJavaResourcesTransform這個類實際上負責多種類型的merge操作,通過類型區分,代碼如下:

    if (mergedType == QualifiedContent.DefaultContentType.RESOURCES) {
        acceptedPathsPredicate =
                path -> !path.endsWith(SdkConstants.DOT_CLASS)
                        && !path.endsWith(SdkConstants.DOT_NATIVE_LIBS);
    } else if (mergedType == ExtendedContentType.NATIVE_LIBS) {

transformResourcesWithMergeJavaResForDebug

執行命令:./gradlew transformResourcesWithMergeJavaResForDebug

  • inputs&outputs
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/appcompat-v7-26.1.0.aar/6b443e96f1af9aa241aaa70576c67a57/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/constraint-layout-1.1.3.aar/f44da5c361a1f52801511229596f72e7/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/butterknife-8.5.1.aar/9d5de52440cb778daab09db33955642f/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/com.squareup.okhttp3/okhttp/3.11.0/75966e05a49046ca2ae734e5626f28837a8d1e82/okhttp-3.11.0.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/animated-vector-drawable-26.1.0.aar/9c804d63d6f065a8f9945f9ad94fee0e/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-vector-drawable-26.1.0.aar/4e56cc34abf77378e2b8d16ee237c82d/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/com.jakewharton/butterknife-annotations/8.5.1/bb67dad90bab7cd77a8f7f1b8442b47e3a2326bc/butterknife-annotations-8.5.1.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-v4-26.1.0.aar/3bf8586900bd31e222ef8b68bfd6e744/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-media-compat-26.1.0.aar/267524a16ca7128dd9cef3c19f394439/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-fragment-26.1.0.aar/77cf518e9868987a283f04cec221fefa/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-core-utils-26.1.0.aar/8634ab1afa6a5a1a947a7bd163aba14f/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-core-ui-26.1.0.aar/8902e2a864b44d47c26fbc80fdafe175/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/support-compat-26.1.0.aar/3e4c87483eacfb4c962d7380a59a114d/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/com.android.support/support-annotations/26.1.0/814258103cf26a15fcc26ecce35f5b7d24b73f8/support-annotations-26.1.0.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/com.android.support.constraint/constraint-layout-solver/1.1.3/bde0667d7414c16ed62d3cfe993cff7f9d732373/constraint-layout-solver-1.1.3.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/com.squareup.okio/okio/1.14.0/102d7be47241d781ef95f1581d414b0943053130/okio-1.14.0.jar
input file:/Users/chao.zheng/.gradle/caches/transforms-1/files-1.1/runtime-1.0.0.aar/ed085e7b9476f7a9fef4ffbb323166ba/jars/classes.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/android.arch.lifecycle/common/1.0.0/e414a4cb28434e25c4f6aa71426eb20cf4874ae9/common-1.0.0.jar
input file:/Users/chao.zheng/.gradle/caches/modules-2/files-2.1/android.arch.core/common/1.0.0/a2d487452376193fc8c103dd2b9bd5f2b1b44563/common-1.0.0.jar
---------------------------------------------------
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/incremental/debug-mergeJavaRes/zip-cache
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/mergeJavaRes/debug
  • 源碼

https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/transforms/MergeJavaResourcesTransform.java

  • 主要代碼邏輯

實際上和前一個task transformNativeLibsWithMergeJniLibsForDebug復用的是一套邏輯,只不過在merge的時候一個是lib類型,一個是resource類型。這個task就是將相應依賴的jar里面的resource合并到intermediates/transforms/mergeJavaRes/debug目錄。但是有一點需要注意,merge java resource時會有些文件時默認不會執行merge操作的,如下:

public PackagingOptions() {
        // ATTENTION - keep this in sync with JavaDoc above.
        exclude("/META-INF/LICENSE");
        exclude("/META-INF/LICENSE.txt");
        exclude("/META-INF/MANIFEST.MF");
        exclude("/META-INF/NOTICE");
        exclude("/META-INF/NOTICE.txt");
        exclude("/META-INF/*.DSA");
        exclude("/META-INF/*.EC");
        exclude("/META-INF/*.SF");
        exclude("/META-INF/*.RSA");
        exclude("/META-INF/maven/**");
        exclude("/NOTICE");
        exclude("/NOTICE.txt");
        exclude("/LICENSE.txt");
        exclude("/LICENSE");
        // Exclude version control folders.
        exclude("**/.svn/**");
        exclude("**/CVS/**");
        exclude("**/SCCS/**");
        // Exclude hidden and backup files.
        exclude("**/.*/**");
        exclude("**/.*");
        exclude("**/*~");
        // Exclude index files
        exclude("**/thumbs.db");
        exclude("**/picasa.ini");
        // Exclude javadoc files
        exclude("**/about.html");
        exclude("**/package.html");
        exclude("**/overview.html");
        // Exclude stuff for unknown reasons
        exclude("**/_*");
        exclude("**/_*/**");
        // Merge services
        merge("/META-INF/services/**");
    }

以上的配置是默認不會執行merge操作的。當然這里gradle也提供了一些其他的配置如:

packagingOptions {
    merge ""
    exclude ""
    pickFirsts ""
}

validateSigningDebug

  • 源碼

https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/ValidateSigningTask.java

  • 主要代碼邏輯

看下這個類的注視

/**
 * A validate task that creates the debug keystore if it's missing. It only creates it if it's in
 * the default debug keystore location.
 *
 * It's linked to a given SigningConfig
 */

通過注視可以很明確的知道,這個task就是在沒有keystore時創建一個keystore,也就是在debug的時候,會使用默認的keystore,這也是為什么在debug模式下,沒有配置keystore時,apk仍然能夠安裝使用。

packageDebug

勝利在望,接下來分析最后一個task.

執行命令:./gradlew packageDebug

  • inputs&outputs
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/assets/debug
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/dexMerger/debug/0
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/mergeJavaRes/debug/0.jar
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/transforms/mergeJniLibs/debug/0
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/manifests/full/debug
input file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/res/debug
input file:/Users/chao.zheng/.android/debug.keystore
---------------------------------------------------
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/intermediates/incremental/packageDebug
output file:/Users/chao.zheng/sunday/OpenSpace/TasksPro/app/build/outputs/apk/debug

分析下輸入目錄:

1、assets/debugassets目錄下的文件;

2、dexMerger/debug/0dex 文件;

3、mergeJavaRes/debug/0.jar java resource 文件;

4、mergeJniLibs/debug/0 so文件;

5、manifests/full/debugmanifest文件;

6、res/debugresources 文件;

7、debug.keystore keystore 文件;

輸出文件即為apk文件。

所以packageDebug任務就是打apk包的。

  • 源碼

https://android.googlesource.com/platform/tools/base/+/gradle_3.0.0/build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/PackageAndroidArtifact.java

  • 主要代碼邏輯
    /**
     * Packages the application incrementally. In case of instant run packaging, this is not a
     * perfectly incremental task as some files are always rewritten even if no change has occurred.
     *
     * @param apkData the split being built
     * @param outputFile expected output package file
     * @param changedDex incremental dex packaging data
     * @param changedJavaResources incremental java resources
     * @param changedAssets incremental assets
     * @param changedAndroidResources incremental Android resource
     * @param changedNLibs incremental native libraries changed
     * @throws IOException failed to package the APK
     */
    private void doTask(
            @NonNull ApkData apkData,
            @NonNull File incrementalDirForSplit,
            @NonNull File outputFile,
            @NonNull FileCacheByPath cacheByPath,
            @NonNull Collection<BuildOutput> manifestOutputs,
            @NonNull ImmutableMap<RelativeFile, FileStatus> changedDex,
            @NonNull ImmutableMap<RelativeFile, FileStatus> changedJavaResources,
            @NonNull ImmutableMap<RelativeFile, FileStatus> changedAssets,
            @NonNull ImmutableMap<RelativeFile, FileStatus> changedAndroidResources,
            @NonNull ImmutableMap<RelativeFile, FileStatus> changedNLibs)
            throws IOException {

        ImmutableMap.Builder<RelativeFile, FileStatus> javaResourcesForApk =
                ImmutableMap.builder();
        javaResourcesForApk.putAll(changedJavaResources);

        
        final ImmutableMap<RelativeFile, FileStatus> dexFilesToPackage = changedDex;

        ...

        // 1、創建packager 對象
        try (IncrementalPackager packager =
                new IncrementalPackagerBuilder()
                        .withOutputFile(outputFile)
                        .withSigning(signingConfig)
                        .withCreatedBy(getBuilder().getCreatedBy())
                        .withMinSdk(getMinSdkVersion())
                        // TODO: allow extra metadata to be saved in the split scope to avoid
                        // reparsing
                        // these manifest files.
                        .withNativeLibraryPackagingMode(
                                PackagingUtils.getNativeLibrariesLibrariesPackagingMode(
                                        manifestForSplit.getOutputFile()))
                        .withNoCompressPredicate(
                                PackagingUtils.getNoCompressPredicate(
                                        aaptOptionsNoCompress, manifestForSplit.getOutputFile()))
                        .withIntermediateDir(incrementalDirForSplit)
                        .withProject(getProject())
                        .withDebuggableBuild(getDebugBuild())
                        .withAcceptedAbis(
                                abiFilter == null ? abiFilters : ImmutableSet.of(abiFilter))
                        .withJniDebuggableBuild(getJniDebugBuild())
                        .build()) {
            // 2、更新package相應的內容
            packager.updateDex(dexFilesToPackage);
            packager.updateJavaResources(changedJavaResources);
            packager.updateAssets(changedAssets);
            packager.updateAndroidResources(changedAndroidResources);
            packager.updateNativeLibraries(changedNLibs);
            ...
    }

代碼也很簡單就是創建IncrementalPackager 對象packager,然后對dex、javares、assets、AndroidResources、NativeLib執行更新操作,也就是寫入操作。執行這些update操作,最終都是調用了IncrementalPackager種的update()方法。代碼如下:

    /**
     * Updates files in the archive.
     *
     * @param updates the updates to perform
     * @throws IOException failed to update the archive
     */
    private void updateFiles(@NonNull Set<PackagedFileUpdate> updates) throws IOException {
        ...

        for (File arch : Sets.newHashSet(archives)) {
            mApkCreator.writeZip(arch, pathNameMap::get, name -> !names.contains(name));
        }
    }

看最后一句mApkCreator.writeZip(),上面的update操作,都通過mAplCreator寫入了Apk包中。

至此在debug模式下生成apk的所有tasks都已經分析完畢。對系列文章能看到這里來的,我相信對這個android的編譯過程基本有了比較清晰的認識了。
基本的編譯過程分析完了,后續將再此系列文章基礎上再出兩個主體:1、《Gradle tool 3.0 增量編譯》;2、《MutilDex拆分過程詳細解析》。

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

推薦閱讀更多精彩內容