開發(fā)應(yīng)用時, 通常會有幾個不同的版本。最常見的是有一個測試用的臨時版本和一個生產(chǎn)版本。這些版本通常有不同的設(shè)置,比如不同的URL地址等。除此之外,你可能有一個免費版和一個包含更多功能的付費版。這種情況下,你已經(jīng)有了四個不同的版本:臨時免費版,臨時付費版,生產(chǎn)免費版,生產(chǎn)付費版。不同的版本有不同的配置是非常復(fù)雜的事情。
Gradle有一些規(guī)則和可擴展的概念來解決這一問題。我們已經(jīng)提及Android Studio為每個新建工程創(chuàng)建的debug
和release
兩種build types
。這是另一個稱為product flavors
的概念,它可以管理一個app或者library的不同版本。build types
和product flavors
是結(jié)合起來使用的,這就使管理免費和付費版的臨時和生產(chǎn)應(yīng)用變得簡單。build type
和product flavor
的結(jié)合稱為build variant
(構(gòu)建變體)。
本章我們首先學習build types
,看看它如何使開發(fā)者的工作更加簡單。接下來我們會討論build types
和product flavors
的不同以及如何使用它們。我們還會學習發(fā)布app所必需的signing configurations
(簽名配置),以及如何為每個構(gòu)建變體設(shè)置不同的簽名配置。
本章我們學習如下內(nèi)容:
build types
product flavors
build variants
signing configurations
Build types(構(gòu)建類型)
在Gradle Android插件中,構(gòu)建類型用來定義一個app或者library應(yīng)該如何構(gòu)建。每個構(gòu)建類型可以指定是否包含調(diào)試符號,applicationId是什么,無用的資源是否應(yīng)該移除等等。你可以使用buildTypes
塊來定義構(gòu)建類型。下面是Android Studio創(chuàng)建的標準buildTypes
塊:
android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
新模塊默認的build.gradle
文件配置了一個名為release
的構(gòu)建類型。它只是禁用了移除無用的資源(通過設(shè)置minifyEnabled
為false
),并且定義了ProGuard配置文件的位置。這是為了讓開發(fā)人員可以直接使用ProGuard進行生產(chǎn)構(gòu)建。
release
構(gòu)建類型不是唯一一個自動為你的工程創(chuàng)建好的構(gòu)建類型。每個模塊默認還有一個debug
構(gòu)建類型。它設(shè)置有合理的默認值,你也可以在buildTypes
塊中配置它,覆寫屬性值。
debug
構(gòu)建類型擁有它自己的默認設(shè)置,以使它方便調(diào)試。當你創(chuàng)建自己的構(gòu)建類型時,會有不同的默認值。比如,debug
構(gòu)建類型的debuggable
屬性設(shè)置為true
,而你自己創(chuàng)建的其他類型為false
創(chuàng)建構(gòu)建類型
當默認設(shè)置不滿足需求時,你也可以很容易地創(chuàng)建自定義構(gòu)建類型。你需要做的只是在buildTypes
塊中添加一個新的構(gòu)建類型的對象。下面創(chuàng)建一個staging的構(gòu)建類型:
android {
buildTypes {
staging {
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
buildConfigField "String", "API_URL","\"http://staging.example.com/api\""
}
}
}
staging
構(gòu)建類型定義了一個新的applicationId的后綴,這樣就和debug
、release
版本區(qū)分開來。假設(shè)其他配置使用默認值,這樣每個構(gòu)建類型的applicationId如下:
- Debug:com.package
- Release:com.package
- Staging:com.package.staging
這意味著你可以在設(shè)備上同時安裝release版本和staging版本,而不會引起沖突。staging構(gòu)建類型也有一個版本名稱的后綴,用于區(qū)分同一設(shè)備不同版本的app。buildConfigField
屬性定義了一個URL地址,我們在第二章已經(jīng)見過。
在創(chuàng)建構(gòu)建類型時,你不必完全自定義,你也可以從由其他構(gòu)建類型來初始化。
android {
buildTypes {
staging.initWith(buildTypes.debug)
staging {
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
debuggable = false
}
}
}
initWith()
方法會根據(jù)傳入的構(gòu)建類型來初始化當前構(gòu)建類型。你可以在新的構(gòu)建類型對象中覆寫屬性,或者定義新的屬性。
Source sets(源碼集)
在你創(chuàng)建一個新構(gòu)建類型時,Gradle也會創(chuàng)建一個新的源碼集。源碼集目錄默認和構(gòu)建類型同名,但并不會自動創(chuàng)建。你需要手動創(chuàng)建該目錄才能為該構(gòu)建類型自定義源代碼和資源文件。
下面是標準的debug
、release
構(gòu)建類型,外加自定義的staging
構(gòu)建類型的源碼集目錄結(jié)構(gòu):
app
└── src
├── debug
│ ├── java
│ │ └── com.package
│ │ └── Constants.java
│ ├── res
│ │ └── layout
│ │ └── activity_main.xml
│ └── AndroidManifest.xml
├── main
│ ├── java
│ │ └── com.package
│ │ └── MainActivity.java
│ ├── res
│ │ ├── drawable
│ │ └── layout
│ │ └── activity_main.xml
│ └── AndroidManifest.xml
├── staging
│ ├── java
│ │ └── com.package
│ │ └── Constants.java
│ ├── res
│ │ └── layout
│ │ └── activity_main.xml
│ └── AndroidManifest.xml
└── release
├── java
│ └── com.package
│ └── Constants.java
└── AndroidManifest.xml
這些源碼集帶來巨大的便利,比如你可以為某個構(gòu)建類型覆蓋特定屬性,添加特定的代碼,添加特殊的布局文件或者字符串資源等。
在你為每個構(gòu)建類型添加Java類的時候,需要知道這是互斥的。這意味著如果你在
staging
的源碼集中添加了CustomLogic.java
文件,你可以在debug
或者release
源碼集中添加同一個文件,但是不可以在main
中添加,否則該文件會被定義兩次,從而在構(gòu)建時引發(fā)異常。
資源文件的處理方式和代碼文件不同。圖片和布局文件會覆蓋main中的同名文件,但是values
目錄下的文件(比如strings.xml
)不會。Gradle會將該構(gòu)建類型和main中的values
目錄下的文件進行合并。
舉個例子,假如main中的strings.xml
文件如下:
<resources>
<string name="app_name">TypesAndFlavors</string>
<string name="hello_world">Hello world!</string>
</resources>
staging中的strings.xml
如下:
<resources>
<string name="app_name">TypesAndFlavors STAGING</string>
</resources>
合并后的strings.xml
文件為:
<resources>
<string name="app_name">TypesAndFlavors STAGING</string>
<string name="hello_world">Hello world!</string>
</resources>
以上規(guī)則同樣適用于manifest文件。當你為一個構(gòu)建類型創(chuàng)建manifest文件時,你不必從main中將整個manifest文件拷貝過來,你只需要添加你需要的標簽就可以了。Android插件會將它們合并到一起。
本章的后續(xù)內(nèi)容我們會詳細討論文件合并。
Product flavors
構(gòu)建類型用來為同一個app或者library配置不同的構(gòu)建,與之相反,product flavors用來為同一個app創(chuàng)建不同的版本。典型的例子是一個app有一個免費版本和一個付費版本。另一個常見的場景是一個代理程序,它為幾個客戶構(gòu)建具有相同功能的應(yīng)用程序,只是品牌發(fā)生改變。一個公司制作的應(yīng)用可以被同類型的客戶重復(fù)使用,這在出租車行業(yè)和銀行領(lǐng)域是很常見的。改變的僅僅是顏色,logo,后臺地址。Product flavors極大簡化了創(chuàng)建基于同樣代碼的不同版本的應(yīng)用的過程。
如果你不確定自己需要新的build type還是新的product flavor,你應(yīng)該問問自己是想要為了內(nèi)部使用為應(yīng)用創(chuàng)建一個新的構(gòu)建,還是想要向Google Play發(fā)布一個新的APK。如果你想要在原有的基礎(chǔ)上創(chuàng)建一個全新的可單獨發(fā)布的應(yīng)用,你應(yīng)該選擇product flavors,否則,你應(yīng)該使用build types。
創(chuàng)建product flavors
創(chuàng)建product flavors和創(chuàng)建build types十分相似。你可以通過添加productFlavor
塊來實現(xiàn):
android {
productFlavors {
red {
applicationId 'com.gradleforandroid.red'
versionCode 3
}
blue {
applicationId 'com.gradleforandroid.blue'
minSdkVersion 14
versionCode 4
}
}
}
Product flavors和build types相比有不同的屬性。這是因為product flavors是ProductFlavor
類的對象,就像所有構(gòu)建腳本中的defaultConfig
對象一樣。這意味著defaultConfig
和你所有的product flavors擁有同樣的屬性。
Source sets
和構(gòu)建類型一樣,product flavors也可以有自己的源碼集目錄。你只需要創(chuàng)建一個和flavor同名的文件夾即可。你甚至可以更進一步,創(chuàng)建一個build type和flavor名稱結(jié)合起來的目錄。名稱結(jié)合時,flavor在前,build type在后。比如,你想為release
版本,blue
flavor的app設(shè)置一個不一樣的圖標,則結(jié)合后的源碼集目錄為blueRelease
。該目錄優(yōu)先級高于單獨的release
目錄或者blue
目錄。
復(fù)合的flavor變體
有些時候,你想要更進一步創(chuàng)建product flavor的組合。舉個例子,客戶A和客戶B想同時擁有免費和付費版本的應(yīng)用,并且兩個客戶的商標不同。創(chuàng)建四個不同的flavors會導致重復(fù)的設(shè)置。這時,組合flvors是一個高效的方式,只需要為flavor設(shè)置不同的維度就可以了:
android {
flavorDimensions "color", "price"
productFlavors {
red {
flavorDimension "color"
}
blue {
flavorDimension "color"
}
free {
flavorDimension "price"
}
paid {
flavorDimension "price"
}
}
}
一旦使用了flavorDimensions
,你需要為每個flavor指定一個維度,否則會產(chǎn)生編譯錯誤。flavorDimensions
定義了一組維度,它們的順序很重要。在組合兩個flavors時,它們可能定義了相同的屬性或者資源。這種情況下,維度的順序決定了優(yōu)先使用哪個flavor的配置。在上面的例子中,color維度會覆蓋price維度。維度的順序也決定了構(gòu)建變體的名稱。上例產(chǎn)生的構(gòu)建變體如下:
-
blueFreeDebug
和blueFreeRelease
-
bluePaidDebug
和bluePaidRelease
-
redFreeDebug
和redFreeRelease
-
redPaidDebug
和redPaidRelease
Build variants(構(gòu)建變體)
構(gòu)建變體是build types和product flavors簡單組合的結(jié)果。每當你創(chuàng)建一個新的build type或者product flavor,一個新的變體也會被創(chuàng)建。比如,你有標準的debug
和release
構(gòu)建變量,你又創(chuàng)建了red
和blue
兩個product flavors,這時將會創(chuàng)建下面的構(gòu)建變體:
圖1 Android Studio構(gòu)建變體窗口</center>
這是Android Studio的Build Variants窗口的截圖。該窗口列出了所有的構(gòu)建變體,并允許你切換它們。點擊Run按鈕將會運行你所選擇的構(gòu)建變體。
如果你沒有product flavors,構(gòu)建變體將只包含構(gòu)建類型。你不可以沒有任何構(gòu)建類型。即使你沒有定義自己的構(gòu)建類型,Android插件也會為app或者library創(chuàng)建debug
構(gòu)建類型。
依賴
每個構(gòu)建類型有它自己的依賴。Gradle自動為每個構(gòu)建類型創(chuàng)建一個新的依賴配置。如果你只想為debug版本添加一個日志框架,可以這樣做:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
debugCompile 'de.mindpipe.android:android-logging-log4j:1.0.3'
}
你可以用這種方式將構(gòu)建類型和依賴配置組合起來使用,比如stagingProvided
、releaseApk
等。這使你可以得到非常具體的依賴項。
Product flavor也可以有自己的依賴配置,定義方式同構(gòu)建類型,如freeCompile
等。
然而如果你想為一個包含構(gòu)建類型和Product flavor的構(gòu)建變體添加依賴配置,你需要在configurations
塊中初始化這個配置。下面為freeDebug
變體添加apk
依賴:
configurations {
// Initializes a placeholder for the freeDebugApk dependency configuration.
freeDebugApk {}
}
dependencies {
freeDebugApk fileTree(dir: 'libs', include: ['*.jar'])
}
任務(wù)
Gradle Android插件會為每個構(gòu)建變體創(chuàng)建任務(wù)。新建的Android應(yīng)用默認有debug
和release
構(gòu)建類型,所以你可以使用assembleDebug
和assembleRelease
任務(wù)來創(chuàng)建對應(yīng)的APK,或者使用assemble
任務(wù)同時創(chuàng)建兩個。當你添加新的構(gòu)建類型時,新的任務(wù)也會被創(chuàng)建。一旦你添加了flavors,一系列新的任務(wù)也會被創(chuàng)建。因為構(gòu)建類型的任務(wù)需要和product flavor的任務(wù)結(jié)合起來。這意味著對于只有一個構(gòu)建類型和一個flavor的簡單配置,你已經(jīng)有了三個任務(wù)來構(gòu)建所有的構(gòu)建變體:
-
assembleBlue使用
blue
flavor的配置,同時assembleBlueRelease
和BlueDebug
-
assembleDebug使用
debug
構(gòu)建類型的配置,為每個product flavor assemble一個debug版本。 -
assembleBlueDebug結(jié)合了
blue
flavor和debug
構(gòu)建類型的配置,并且flavor設(shè)置會覆蓋構(gòu)建類型的設(shè)置。
Source sets
由一個構(gòu)建類型和一個或多個product flavor組成的構(gòu)建變體也可以有自己的源碼集目錄。比如由debug
構(gòu)建類型,blue
flavor和free
flavor組成的構(gòu)建變體,源碼集目錄為src/blueFreeDebug/java/
。你也可以在sourceSets
塊中修改目錄位置,這在第一章出現(xiàn)過。
合并資源和manifest文件
源碼集的引入增加了構(gòu)建過程的復(fù)雜度。Gradle Android插件在打包app前需要合并main源碼集和構(gòu)建類型的源碼集。除此之外,庫工程也可能提供額外資源,它們也需要合并進來。這同樣適用于manifest文件。比如,你在debug變體的app中可能需要額外的Android權(quán)限來保存日志文件,而你并不想在main源碼集中聲明這些權(quán)限,因為這可能引起潛在用戶的抵觸。作為替代方案,你需要在debug
構(gòu)建類型的源碼集中添加manifest文件來聲明這些權(quán)限。
資源和manifest文件的優(yōu)先級順序如下:
Build type > Flavor > Main > Dependencies
如果一個資源同時在flavor和main的源碼集中聲明,那么flavor中的資源有更高的優(yōu)先級。這種情況下,flavor源碼集中的資源會被打包,main源碼集中的不會。庫工程中聲明的資源優(yōu)先級最低。
合并資源和manifest文件還有許多需要學習的地方。如果你想學習更多細節(jié),可以閱讀官方文檔:http://tools.android.com/tech-docs/new-build-system/user-guide/manifest-merger
創(chuàng)建構(gòu)建變體
Gradle可以很容易地處理復(fù)雜的構(gòu)建變體。即使是創(chuàng)建并配置兩個構(gòu)建類型和兩個product flavors,構(gòu)建文件依然很簡潔:
android {
buildTypes {
debug {
buildConfigField "String", "API_URL","\"http://test.example.com/api\""
}
staging.initWith(android.buildTypes.debug)
staging {
buildConfigField "String", "API_URL","\"http://staging.example.com/api\""
applicationIdSuffix ".staging"
}
}
productFlavors {
red {
applicationId "com.gradleforandroid.red"
resValue "color", "flavor_color", "#ff0000"
}
blue {
applicationId "com.gradleforandroid.blue"
resValue "color", "flavor_color", "#0000ff"
}
}
}
本例我們創(chuàng)建了四個構(gòu)建變體:blueDebug,blueStaging,redDebug
和redStaging
。
變體過濾器
你也可以在構(gòu)建中完全忽略某個變體。這樣,你就可以加快assemble
命令構(gòu)建所有變體的速度,并且去掉不用執(zhí)行的任務(wù)。這也可以保證在Android Studio的構(gòu)建變體窗口(見圖1)中不會出現(xiàn)這個變體的選項。
你可以在build.gradle
文件中添加如下代碼來過濾掉構(gòu)建變體:
android.variantFilter { variant ->
if(variant.buildType.name.equals('release')) {
variant.getFlavors().each() { flavor ->
if (flavor.name.equals('blue')) {
variant.setIgnore(true);
}
}
}
}
本例中,我們首先檢查變體的構(gòu)建類型是不是release
,然后我們檢查該構(gòu)建類型的所有flavors。getFlavors()
方法返回一個flavor數(shù)組,數(shù)組的長度等于flavor的維度數(shù)。比如,對于blueFreeDebug
變體而言,flavor數(shù)組包含blue
和free
兩個flavor。本例過濾掉了blueFreeRelease
和bluePaidRelease
兩個變體。運行gradlew tasks
命令,將不會看到這兩個變體相關(guān)的任務(wù)。
簽名配置
在你將應(yīng)用發(fā)布到Google Play或其他應(yīng)用市場之前,你需要用私鑰進行簽名。如果你有多個不同的版本,你需要為每個flavor使用不同的私鑰進行簽名。這就需要使用簽名配置了。
android {
signingConfigs {
staging.initWith(signingConfigs.debug)
release {
storeFile file("release.keystore")
storePassword"secretpassword"
keyAlias "gradleforandroid"
keyPassword "secretpassword"
}
}
}
本例我們創(chuàng)建了兩個不同的簽名配置。
Android插件會默認配置一個名為debug
的簽名配置,使用一個通用的公開密碼的keystore文件,所以沒有必要再為debug構(gòu)建類型創(chuàng)建一個簽名配置。
staging
配置調(diào)用了initWith()
函數(shù),該函數(shù)通過傳入的配置初始化當前配置。這就表示staging
使用了和debug
相同的簽名配置。
release
配置使用storeFile
來指定keystore文件的路徑,同時定義了key別名和用到的密碼。
就像前面提到的,在構(gòu)建文件中保存證書不是很好的設(shè)計。推薦保存在Gradle properties文件中。第七章會用一大塊去講解處理簽名配置密碼的一個任務(wù)。
在定義了簽名配置之后,你需要將其應(yīng)用到構(gòu)建類型或者flavor中。構(gòu)建類型和flavor都有一個名為signingConfig
的屬性:
android {
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
本例使用了構(gòu)建類型。如果你想為每個flavor使用不同的證書,你需要創(chuàng)建不同的簽名配置。你可以用同樣的方式定義它們:
android {
productFlavors {
blue {
signingConfig signingConfigs.release
}
}
}
通過這種方式使用簽名配置會出現(xiàn)問題。為flavor設(shè)置簽名配置時,會覆蓋構(gòu)建類型的簽名配置。更好的方式是為每個構(gòu)建類型每個flavor設(shè)置不同的配置:
android {
buildTypes {
release {
productFlavors.red.signingConfig signingConfigs.red
productFlavors.blue.signingConfig signingConfigs.blue
}
}
}