Android 中的 Gradle 配置介紹

目錄

Android Studio 目錄層級

├── app #Android App目錄
│   ├── app.iml
│   ├── build #構建輸出目錄
│   ├── build.gradle #構建腳本
│   ├── libs #so相關庫
│   ├── proguard-rules.pro #proguard混淆配置
│   └── src #源代碼,資源等
├── build
│   └── intermediates
├── build.gradle #工程構建文件
├── gradle
│   └── wrapper
├── gradle.properties #gradle的配置 外部屬性
├── gradlew #gradle wrapper linux shell腳本
├── gradlew.bat
├── LibSqlite.iml
├── local.properties #配置Androod SDK位置文件
└── settings.gradle #工程配置

settings.gradle

settings.gradle 用于配置 project。settings 文件聲明了所需的配置來實例化項目的層次結構,標明其下有幾個 module,比如這里包含一個:appmodule。

include ':app'

根目錄的 build.gradle

settings.gradle 在同一目錄下的 build.gradle 是一個頂級的 build 配置文件,在這里可以為所有的 project 以及 module 配置一些常用的配置。

根目錄中的 build.gradle 默認包含 2 個代碼塊

  • buildscript{} 用于配置構建腳本所用到的代碼庫和依賴關系。這里定義了 Android 編譯工具的類路徑。repositories 中, jCenter 是一個著名的 Maven 倉庫。
  • allprojects{} 用于定義所有模塊需要用到的一些公共屬性。這里定義的屬性會被應用到所有的 module 中,但為了保證每個項目的獨立性,一般不會在這里操作太多共有的東西
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()//使用 jcenter庫
    }
    dependencies {
        // 依賴 android提供的 1.1.0 的 gradle build
        classpath 'com.android.tools.build:gradle:1.1.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
//為所有的工程的 repositories 配置為 jcenters
allprojects {
    repositories {
        jcenter()
    }
}

模塊級 build.gradle

除了更目錄下的 build 文件,gradle 支持對每個 module 進行配置,主要有三個重要的代碼塊:plugin、android 和 dependencies

apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "org.flysnow.demo"
        minSdkVersion 9
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.0.0'
}

apply plugin

第一行 apply plugin: 'com.android.application',這表示該 module 是一個 app module,應用了com.android.application 插件。
如果是一個 android library,那么這里的是 apply plugin: 'com.android.library'

  • apply plugin:聲明引用插件的類型。如果是庫的話就加
  • apply from:表示引用其他的配置文件,比如 apply from:"config.gradle"

android

compileSdkVersion & buildToolsVersion

  • compileSdkVersion: 基于哪個 SDK 編譯,這里的 Version 是 API LEVEL,是21
  • buildToolsVersion: 基于哪個構建工具版本進行構建的

defaultConfig

defaultConfig 是默認配置,如果沒有其他的配置覆蓋,就會使用這里的。

常用屬性:

  • applicationId 配置包名的

  • versionCode 版本號

  • versionName 版本名稱

  • minSdkVersion app能夠運行的最小版本

  • targetSdkVersion 目標設備sdk(向前兼容)

    一般 minSdkVersion < targetSdkVersion <= compileSdkVersion
    miniSdkVersion: app能運行的最小版本,比如 minSdkVersion=18(Android 4.3)那么在低于這個系統版本的設備上就會提示不能安裝或運行。

    compileSdkVersion: 是我們告訴 Gradle,我們是用哪一版本的Android Sdk去編譯程序的,可以使用這個版本的 API,比如我們使用的是 7.0 的版本
    compileSdkVersion=24,那么我們對于拍照裁剪圖片等功能的操作,就可以使用 FileProvider了。

    targetSdkVersion: 系統通過 targetSdkVersion 來保證 Android 的向前兼容性,在 Android4.4 之后的設備上,系統會判斷你的 targetSdkVersion 是否小于 19,如果小于的話,那就按照 19 之前的 api 方法,如果大于等于 19,那么就按照之后的 api 方法來走,保證了程序運行的一致性。也就是向前兼容性。

defaultConfig{} 還有很多其他的配置,后面介紹

buildTypes

buildTypes 是構建類型,常用的有 releasedebug 兩種,可以在這里面啟用混淆,啟用 zipAlign 以及配置簽名信息等。

buildTypes{} 中的其他配置,后面介紹

dependencies

dependencies 就不屬于 Android 專有的配置了,它定義了該 module 需要依賴的 jar,aar,jcenter庫信息。

apply plugin: 'com.android.application'

android { ... }

dependencies {
    // Dependency on a local library module
    implementation project(":mylibrary")

    // Dependency on local binaries
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    // Dependency on a remote binary
    implementation 'com.example.android:app-magic:12.3'
}

本地項目依賴

implementation project(':mylibrary')

這聲明了對一個名為“mylibrary”(此名稱必須與在您的 settings.gradle 文件中使用 include: 定義的庫名稱相符)的 Android 庫模塊的依賴關系。在構建您的應用時,構建系統會編譯該庫模塊,并將生成的編譯內容打包到 APK 中。

本地二進制文件依賴項

implementation fileTree(dir: 'libs', include: ['*.jar'])

Gradle 聲明了對項目的 module_name/libs/ 目錄中 JAR 文件的依賴關系(因為 Gradle 會讀取 build.gradle 文件的相對路徑)。

或者,您也可以按如下方式指定各個文件:

implementation files('libs/foo.jar', 'libs/bar.jar')

遠程二進制文件依賴項

implementation 'com.example.android:app-magic:12.3'

這實際上是以下代碼的簡寫形式:

implementation group: 'com.example.android', name: 'app-magic', version: '12.3'

這聲明了對“com.example.android”命名空間組內的 12.3 版“app-magic”庫的依賴關系。

注意:此類遠程依賴項要求您聲明適當的遠程代碼庫,Gradle 應在其中查找相應的庫。如果本地不存在相應的庫,那么當 build 需要它時(例如,當您點擊 Sync Project with Gradle Files 圖標 [站外圖片上傳中...(image-aa6d99-1607933369919)] 或運行 build 時),Gradle 會從遠程站點提取它。

依賴項配置

  • implementation: Gradle 會將依賴項添加到編譯類路徑和構建輸出,依賴不會暴露給其他模塊
  • api:Gradle 會將依賴項添加到編譯類路徑和構建輸出,依賴會傳遞給其他依賴該模塊的模塊
  • compileOnly:Gradle 只會將依賴項添加到編譯類路徑,僅在編譯期依賴,運行時可有可無時可以使用該配置
  • runtimeOnly:Gradle 只會將依賴項添加到構建輸出,以便在運行時使用。也就是說,不會將其添加到編譯類路徑。
    annotationProcessor:添加注解處理器的庫依賴關系,需使用該配置將其添加到注解處理器類路徑。注:Kotlin 使用 kapt 聲明注解處理器依賴項

以上配置可以將依賴項應用于所有構建變體

如果想為特定的構建變體源代碼集或測試源代碼集聲明依賴,必須將配置的名稱首字母大寫,并在前面加上構建變體或測試源代碼集的名稱作為前綴

dependencies {
    freeImplementation 'com.google.firebase:firebase-ads:9.8.0'
}

不過,如果您想為將產品變種和構建類型組合在一起的變體添加依賴項,就必須在 configurations 代碼塊中初始化配置名稱。以下示例向“freeDebug”構建變體添加了 runtimeOnly 依賴項(使用本地二進制文件依賴項):

configurations {
    // Initializes a placeholder for the freeDebugRuntimeOnly dependency
    // configuration.
    freeDebugRuntimeOnly {}
}

dependencies {
    freeDebugRuntimeOnly fileTree(dir: 'libs', include: ['*.jar'])
}

ext{} 代碼塊 定制項目屬性

在根目錄 build.gradle 中, 可以定制適用于所有模塊的屬性,通過 ext 代碼塊來實現,如:

ext {
    compileSdkVersion = 28
    minSdkVersion = 18
}

然后在模塊的 build.gradle 配置引用這些屬性,語法為:
rootProject.ext.{屬性名}, 比如:

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
} 

構建類型 android.buildTypes{}

默認情況下,Android Plugin 會自動給項目構建 debugrelease 版本。兩個版本的區別在于能否在安全設備(非 dev)上調試,以及 APK 如何簽名。debug 使用通過通用的 name/password 對生成的密鑰證書進行簽名(為了防止在構建過程中出現認證請求)。release 在構建過程中不進行簽名,需要自行簽名。

這些配置是通過 BuildType 對象來完成的。默認情況下,debugrelease 實例都會被創建。Android plugin 允許像創建其他 Build Type 一樣自定義這兩種類型。在 buildTypes 的 DSL 容器中進行配置:

android {
    buildTypes {
        debug {
            applicationIdSuffix ".debug"
        }

        jnidebug {
            initWith(buildTypes.debug)// 復制
            packageNameSuffix ".jnidebug"
            jnidebugBuild true
        }
    }
}

另外,每個 Build Type 都會創建一個新的 assemble<BuildTypeName> 任務。
例如:assembleDebug。
當 debug 和 release 構建類型被預創建的時候,assembleDebug 和 assembleRelease 會被自動創建。

buildType 的屬性和方法

https://developer.android.com/reference/tools/gradle-api

屬性 描述
applicationIdSuffix 應用 id 后綴
name build type的名字
versionNameSuffix 版本名稱后綴
minifyEnabled 是否混淆
proguardFiles 混淆文件
signingConfig 簽名配置
multiDexEnabled 是否拆成多個Dex
方法 描述
buildConfigField(type, name, value) 添加一個變量生成 BuildConfig 類
consumerProguardFile(proguardFile) 添加一個混淆文件進arr包
consumeProguardFile(proguardFiles) 添加混淆文件進arr包
externalNativeBuild(action) 配置本地的build選項
initWith(that) 復制這個 build 類型的所有屬性
proguardFile(proguardFile) 添加一個新的混淆配置文件
proguradFiles(files) 添加新的混淆文件
resValue(type, name, value) 添加一個新的生成資源
setProguardFiles(proguardFileIterable) 設置一個混淆配置文件

啟用 proguard 混淆

可以為不同的 buildTypes 選擇是否啟用混淆,一般 release 發布版本是需要啟用混淆的,這樣別人反編譯之后就很難分析你的代碼,而我們自己開發調試的時候是不需要混淆的,所以 debug 不啟用混淆。對 release 啟用混淆的配置如下:

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFile 'proguard.cfg'
        }
   }
}
  • minifyEnabled 為 true 表示啟用混淆
  • proguardFile 是混淆使用的配置文件
    這里是 module 根目錄下的 proguard.cfg 文件

啟用 zipAlign

這個也是比較簡單的,同樣也是在 buildTypes 里配置,可以為不用的 buildTypes 選擇時候開啟zipAlign

android {
    buildTypes {
        release {
            zipAlignEnabled true
        }
   }
}

配置應用的簽名信息 android.signingConfigs{}

android.signingConfigs{} 下定義一個或者多個簽名信息,在 buildTypes{} 配置使用即可。比如:

android {
    signingConfigs {
        release {
            storeFile file("release.keystore")
            keyAlias "tina"
            keyPassword "123456"
            storePassword "123456"
        }
        debug {
            ...
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
        debug {
            signingConfig signingConfigs.debug
        }
    }
  }
  • storeFile:簽名證書文件
  • keyAlias:別名
  • keyPassword:key的密碼
  • storePassword:證書的密碼

配好相關信息即可在 buildTypes 配置使用

多渠道打包 android.productFlavors{}

Android Gradle 給我們提供了productFlavors,讓我們可以對生成的 APK 包進行定制,所以就有了多渠道。它支持與 defaultConfig 相同的屬性,這是因為,defaultConfig 實際上屬于ProductFlavor

android  {
    productFlavors {
        dev{

        }
        google{

        }
        baidu{

        }
    }
}

這樣當我們運行 assembleRelease 的時候就會生成 3 個 release 包,分別是 dev、google 以及 baidu。目前看這三個包除了文件名沒有什么不一樣,此時還沒有定制,使用的都是 defaultConfig 配置。這里的 flavor 和 defaultConfig 是一樣的,可以自定義其 applicationId、versionCode 以及 versionName等信息,比如區分不同包名:

android  {
    defaultConfig{
        ……
        // Specifies a flavor dimension.
        flavorDimensions "channel"
    }
    productFlavors {
        dev{
            applicationId "cn.tina.demo.dev"
        }
        google{
            applicationId "cn.tina.demo.google"
        }
    }
}

flavorDimensions(風格維度),所有變種都必須屬于一個指定的變種維度,即一個產品變種組。必須將所有變種分配給某個變種維度;否則,您將收到如下所示的構建錯誤。如果給定的模塊僅指定一個變種維度,則 Android Gradle 插件會自動將該模塊的所有變種分配給該維度。

 Error:All flavors must now belong to a named flavor dimension.
 The flavor 'flavor_name' is not assigned to a flavor dimension.

注意:flavor 的命名不能與已存在的 Build Type 或者與 androidTest、test sourceSet 有沖突。

每一個 Build Type 都會生成新的 APK。Product Flavors 同樣也會做這些事情:項目的輸出將會組合所有的 Build Types 和 Product Flavors(如果有定義 Flavor)。

每一種組合(包含 Build Type 和 Product Flavor)就是一個 Build Variant(構建變種版本)。

例如,在之前的 Flavor 聲明例子中與默認的 debug 和 release 兩個 Build Types 將會生成 4 個 Build Variants:{dev, google}{debug, release}

  • dev - debug
  • dev - release
  • google - debug
  • google - release

項目中如果沒有定義 flavor 同樣也會有 Build Variants,只是使用的是 default 的 flavor/config,因為是無名稱的,所以生成的 build variant 列表看起來就跟 Build Type 列表一樣。

多定制的變種版本

擴展:flavorDimesions 可以設置多個,如

android  {
   defaultConfig{
       ……
       // Specifies a flavor dimension.
       flavorDimensions "channel","production"
   }
   productFlavors {
       dev{
           dimension "channel"
           applicationId "cn.tina.demo.dev"
       }
       google{
           dimension "channel"
           applicationId "cn.tina.demo.google"
       }
       productA{
         dimension "production"  
       }
       productB{
         dimension "production"  
       }
   }
}

那么可以組合出4種(2*2)種產品,分別有 debug 和 release,一共是8個變種版本

構建和任務

我們前面提到每一個 Build Type 會創建自己的 assemble<name> task,但是 Build Variants 是 Build Type 和 Product Flavor 的組合。
當使用 Product Flavor 的時候,將會創建更多的 assemble-type task。分別是:

  • assemble<Variant Name> 允許直接構建一個 variant 版本,例如 assembleDevDebug

  • assemble<Build Type Name> 允許構建指定 Build Type 的所有 APK,例如 assembleDebug 將會構建 DevDebugGoogleDebug 兩個 variant 版本。

  • assemble<Product Flavor Name> 允許構建指定 flavor 的所有 APK,例如 assembleDev 將會構建 DevDebugDevRelease 兩個 variant 版本。
    另外 assemble task 會構建所有的 variant 版本。

android.defaultConfig

AndroidManifest 里的占位符

AndroidManifest.xml 這是一個很重要的文件,我們的很多配置都在這里定義。有時候我們的一些配置信息,比如一個第三方應用的 key,第三方統計分析的渠道號等也要在這里進行配置。

這里以友盟統計分析平臺為例,演示這一功能的使用。在友盟統計分析中,我們需要根據渠道進行統計,比如 google,百度,應用寶等渠道的活躍新增等。

友盟的 SDK 是在 AndroidManifest 里配置一個 name 為 UMENG_CHANNEL 的 meta-data,這樣這個 meta-data 的值就表示這個 apk 是哪個渠道,我們版本發布有幾十個渠道,manifestPlaceholders 允許動態替換在 AndroidManifest 文件里定義的占位符。

<meta-data 
    android:value="${UMENG_CHANNEL_VALUE}"
    android:name="UMENG_CHANNEL"
/>

如上 ${UMENG_CHANNEL_VALUE} 就是一個占位符,然后我們在 gradle 的 defaultConfig 里這樣定義腳本:

android {
    defaultConfig {
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: 'dev']
    }
}

上面代碼塊的意思就是我們的默認配置里 AndroidManifest 的 ${UMENG_CHANNEL_VALUE} 占位符會被 dev 這個字符串所替換,也就說默認運行的版本是一個開發版。以此類推,我們其他渠道的版本就可以這樣定義:

android  {
    productFlavors {
        google{
            applicationId "org.demo.google"
            manifestPlaceholders.put("UMENG_CHANNEL_VALUE",'google')
        }
        baidu{
            applicationId "org.demo.baidu"
            manifestPlaceholders.put("UMENG_CHANNEL_VALUE",'baidu')
        }
    }
}

這樣有多少個渠道就做多少次這樣的定義,即可完成分渠道統計。但是如果上百個渠道,這樣一個個寫的確太累,很麻煩,我們繼續研究,同學們有沒有發現,我們的渠道名字和我們的 flavorName 一樣,我們用這個 flavorName 作為 UMENG_CHANNEL_VALUE 不就好了嗎,可以批量的替換嗎?當然可以,這又體現了我們 Gradle 的強大和靈活之處。

productFlavors.all { flavor ->
        manifestPlaceholders.put("UMENG_CHANNEL_VALUE",name)
}

循環每個 flavor,并把他們的 UMENG_CHANNEL_VALUE 設置為他們自己的 name 名字。

BuildConfig 自定義常量

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String BUILD_TYPE = "debug";
}

BuildConfig.java 是 Android Gradle 自動生成的一個 java 類文件,無法手動編譯,但是可以通過 Gradle 控制,也就是說這里是動態可配置的,這里以生產環境和測試環境為例來說明該功能的使用。

我們在開發 App 的時候免不了要和服務器進行通信,我們的服務器一般都有生產和測試環境,當我們處理開發和測試的時候使用測試環境進行調試,正式發布的時候使用生成環境。以前的時候我們通過把不同的配置文件打包進 APK 中來控制,現在不一樣了,我們有更簡便的方法,這就是 buildConfigField

android {
    defaultConfig {
        buildConfigField'String','API_SERVER_URL','"http://test.cn/"'
    }
    productFlavors {
        google{
            buildConfigField 'String','API_SERVER_URL','"http://release.cn/"'
        }
        baidu{
            buildConfigField 'String','API_SERVER_URL','"http://release.cn/"'
        }
    }
}

buildConfigField 一共有3個參數,

  • 第一個是數據類型,是定義的常量值是一個什么類型,和 Java 的類型是對等的,這里是String。
  • 第二個參數是常量名,這里是 API_SERVER_URL
  • 第三個參數是常量值。如此定義之后,就會在 BuildConfig.java 中生成一個常量名為 API_SERVER_URL 的常量定義。默認配置的生成是:
public final static String API_SERVER_URL = "http://test.cn/"

當是 baidu 和 google 渠道的時候生成的就是 http://release.cn/ 了。這個常量可以在我們編碼中引用。在我們進行打包的時候會根據 Gradle 配置動態替換。

我們發現一般渠道版本都是用來發布的,肯定用的是生產服務器,所以我們可以使用批處理來搞定這個事情,而不用在一個個渠道里寫這些配置。

productFlavors.all { flavor ->
        buildConfigField 'String','API_SERVER_URL','"http://release.org/"'
    }

此外,比如 Gradle 的 resValue,也和 buildConfigField 一樣,只不過控制生成的是資源,比如我們在 android 的 values.xml 定義生成的字符串。可以用它來動態生成我們想要的字符串,比如應用的名字,可能一些渠道會不一樣,這樣就可以很靈活的控制自動生成,關于 resValue 詳細介紹請參考相關文檔,這里不再舉例說明。

參考:https://developer.android.com/studio/build

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

推薦閱讀更多精彩內容