开发应用时, 通常会有几个不同的版本。最常见的是有一个测试用的临时版本和一个生产版本。这些版本通常有不同的设置,比如不同的URL地址等。除此之外,你可能有一个免费版和一个包含更多功能的付费版。这种情况下,你已经有了四个不同的版本:临时免费版,临时付费版,生产免费版,生产付费版。不同的版本有不同的配置是非常复杂的事情。
Gradle有一些规则和可扩展的概念来解决这一问题。我们已经提及Android Studio为每个新建工程创建的debug
和release
两种build types
。这是另一个称为product flavors
的概念,它可以管理一个app或者library的不同版本。build types
和product flavors
是结合起来使用的,这就使管理免费和付费版的临时和生产应用变得简单。build type
和product flavor
的结合称为build variant
(构建变体)。
本章我们首先学习build types
,看看它如何使开发者的工作更加简单。接下来我们会讨论build types
和product flavors
的不同以及如何使用它们。我们还会学习发布app所必需的signing configurations
(签名配置),以及如何为每个构建变体设置不同的签名配置。
本章我们学习如下内容:
build types
product flavors
build variants
signing configurations
Build types(构建类型)
在Gradle Android插件中,构建类型用来定义一个app或者library应该如何构建。每个构建类型可以指定是否包含调试符号,applicationId是什么,无用的资源是否应该移除等等。你可以使用buildTypes
块来定义构建类型。下面是Android Studio创建的标准buildTypes
块:
android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
新模块默认的build.gradle
文件配置了一个名为release
的构建类型。它只是禁用了移除无用的资源(通过设置minifyEnabled
为false
),并且定义了ProGuard配置文件的位置。这是为了让开发人员可以直接使用ProGuard进行生产构建。
release
构建类型不是唯一一个自动为你的工程创建好的构建类型。每个模块默认还有一个debug
构建类型。它设置有合理的默认值,你也可以在buildTypes
块中配置它,覆写属性值。
debug
构建类型拥有它自己的默认设置,以使它方便调试。当你创建自己的构建类型时,会有不同的默认值。比如,debug
构建类型的debuggable
属性设置为true
,而你自己创建的其他类型为false
创建构建类型
当默认设置不满足需求时,你也可以很容易地创建自定义构建类型。你需要做的只是在buildTypes
块中添加一个新的构建类型的对象。下面创建一个staging的构建类型:
android {
buildTypes {
staging {
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
buildConfigField "String", "API_URL","\"http://staging.example.com/api\""
}
}
}
staging
构建类型定义了一个新的applicationId的后缀,这样就和debug
、release
版本区分开来。假设其他配置使用默认值,这样每个构建类型的applicationId如下:
- Debug:com.package
- Release:com.package
- Staging:com.package.staging
这意味着你可以在设备上同时安装release版本和staging版本,而不会引起冲突。staging构建类型也有一个版本名称的后缀,用于区分同一设备不同版本的app。buildConfigField
属性定义了一个URL地址,我们在第二章已经见过。
在创建构建类型时,你不必完全自定义,你也可以从由其他构建类型来初始化。
android {
buildTypes {
staging.initWith(buildTypes.debug)
staging {
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
debuggable = false
}
}
}
initWith()
方法会根据传入的构建类型来初始化当前构建类型。你可以在新的构建类型对象中覆写属性,或者定义新的属性。
Source sets(源码集)
在你创建一个新构建类型时,Gradle也会创建一个新的源码集。源码集目录默认和构建类型同名,但并不会自动创建。你需要手动创建该目录才能为该构建类型自定义源代码和资源文件。
下面是标准的debug
、release
构建类型,外加自定义的staging
构建类型的源码集目录结构:
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
这些源码集带来巨大的便利,比如你可以为某个构建类型覆盖特定属性,添加特定的代码,添加特殊的布局文件或者字符串资源等。
在你为每个构建类型添加Java类的时候,需要知道这是互斥的。这意味着如果你在
staging
的源码集中添加了CustomLogic.java
文件,你可以在debug
或者release
源码集中添加同一个文件,但是不可以在main
中添加,否则该文件会被定义两次,从而在构建时引发异常。
资源文件的处理方式和代码文件不同。图片和布局文件会覆盖main中的同名文件,但是values
目录下的文件(比如strings.xml
)不会。Gradle会将该构建类型和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>
以上规则同样适用于manifest文件。当你为一个构建类型创建manifest文件时,你不必从main中将整个manifest文件拷贝过来,你只需要添加你需要的标签就可以了。Android插件会将它们合并到一起。
本章的后续内容我们会详细讨论文件合并。
Product flavors
构建类型用来为同一个app或者library配置不同的构建,与之相反,product flavors用来为同一个app创建不同的版本。典型的例子是一个app有一个免费版本和一个付费版本。另一个常见的场景是一个代理程序,它为几个客户构建具有相同功能的应用程序,只是品牌发生改变。一个公司制作的应用可以被同类型的客户重复使用,这在出租车行业和银行领域是很常见的。改变的仅仅是颜色,logo,后台地址。Product flavors极大简化了创建基于同样代码的不同版本的应用的过程。
如果你不确定自己需要新的build type还是新的product flavor,你应该问问自己是想要为了内部使用为应用创建一个新的构建,还是想要向Google Play发布一个新的APK。如果你想要在原有的基础上创建一个全新的可单独发布的应用,你应该选择product flavors,否则,你应该使用build types。
创建product flavors
创建product flavors和创建build types十分相似。你可以通过添加productFlavor
块来实现:
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
类的对象,就像所有构建脚本中的defaultConfig
对象一样。这意味着defaultConfig
和你所有的product flavors拥有同样的属性。
Source sets
和构建类型一样,product flavors也可以有自己的源码集目录。你只需要创建一个和flavor同名的文件夹即可。你甚至可以更进一步,创建一个build type和flavor名称结合起来的目录。名称结合时,flavor在前,build type在后。比如,你想为release
版本,blue
flavor的app设置一个不一样的图标,则结合后的源码集目录为blueRelease
。该目录优先级高于单独的release
目录或者blue
目录。
复合的flavor变体
有些时候,你想要更进一步创建product flavor的组合。举个例子,客户A和客户B想同时拥有免费和付费版本的应用,并且两个客户的商标不同。创建四个不同的flavors会导致重复的设置。这时,组合flvors是一个高效的方式,只需要为flavor设置不同的维度就可以了:
android {
flavorDimensions "color", "price"
productFlavors {
red {
flavorDimension "color"
}
blue {
flavorDimension "color"
}
free {
flavorDimension "price"
}
paid {
flavorDimension "price"
}
}
}
一旦使用了flavorDimensions
,你需要为每个flavor指定一个维度,否则会产生编译错误。flavorDimensions
定义了一组维度,它们的顺序很重要。在组合两个flavors时,它们可能定义了相同的属性或者资源。这种情况下,维度的顺序决定了优先使用哪个flavor的配置。在上面的例子中,color维度会覆盖price维度。维度的顺序也决定了构建变体的名称。上例产生的构建变体如下:
-
blueFreeDebug
和blueFreeRelease
-
bluePaidDebug
和bluePaidRelease
-
redFreeDebug
和redFreeRelease
-
redPaidDebug
和redPaidRelease
Build variants(构建变体)
构建变体是build types和product flavors简单组合的结果。每当你创建一个新的build type或者product flavor,一个新的变体也会被创建。比如,你有标准的debug
和release
构建变量,你又创建了red
和blue
两个product flavors,这时将会创建下面的构建变体:
图1 Android Studio构建变体窗口</center>
这是Android Studio的Build Variants窗口的截图。该窗口列出了所有的构建变体,并允许你切换它们。点击Run按钮将会运行你所选择的构建变体。
如果你没有product flavors,构建变体将只包含构建类型。你不可以没有任何构建类型。即使你没有定义自己的构建类型,Android插件也会为app或者library创建debug
构建类型。
依赖
每个构建类型有它自己的依赖。Gradle自动为每个构建类型创建一个新的依赖配置。如果你只想为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'
}
你可以用这种方式将构建类型和依赖配置组合起来使用,比如stagingProvided
、releaseApk
等。这使你可以得到非常具体的依赖项。
Product flavor也可以有自己的依赖配置,定义方式同构建类型,如freeCompile
等。
然而如果你想为一个包含构建类型和Product flavor的构建变体添加依赖配置,你需要在configurations
块中初始化这个配置。下面为freeDebug
变体添加apk
依赖:
configurations {
// Initializes a placeholder for the freeDebugApk dependency configuration.
freeDebugApk {}
}
dependencies {
freeDebugApk fileTree(dir: 'libs', include: ['*.jar'])
}
任务
Gradle Android插件会为每个构建变体创建任务。新建的Android应用默认有debug
和release
构建类型,所以你可以使用assembleDebug
和assembleRelease
任务来创建对应的APK,或者使用assemble
任务同时创建两个。当你添加新的构建类型时,新的任务也会被创建。一旦你添加了flavors,一系列新的任务也会被创建。因为构建类型的任务需要和product flavor的任务结合起来。这意味着对于只有一个构建类型和一个flavor的简单配置,你已经有了三个任务来构建所有的构建变体:
-
assembleBlue使用
blue
flavor的配置,同时assembleBlueRelease
和BlueDebug
-
assembleDebug使用
debug
构建类型的配置,为每个product flavor assemble一个debug版本。 -
assembleBlueDebug结合了
blue
flavor和debug
构建类型的配置,并且flavor设置会覆盖构建类型的设置。
Source sets
由一个构建类型和一个或多个product flavor组成的构建变体也可以有自己的源码集目录。比如由debug
构建类型,blue
flavor和free
flavor组成的构建变体,源码集目录为src/blueFreeDebug/java/
。你也可以在sourceSets
块中修改目录位置,这在第一章出现过。
合并资源和manifest文件
源码集的引入增加了构建过程的复杂度。Gradle Android插件在打包app前需要合并main源码集和构建类型的源码集。除此之外,库工程也可能提供额外资源,它们也需要合并进来。这同样适用于manifest文件。比如,你在debug变体的app中可能需要额外的Android权限来保存日志文件,而你并不想在main源码集中声明这些权限,因为这可能引起潜在用户的抵触。作为替代方案,你需要在debug
构建类型的源码集中添加manifest文件来声明这些权限。
资源和manifest文件的优先级顺序如下:
Build type > Flavor > Main > Dependencies
如果一个资源同时在flavor和main的源码集中声明,那么flavor中的资源有更高的优先级。这种情况下,flavor源码集中的资源会被打包,main源码集中的不会。库工程中声明的资源优先级最低。
合并资源和manifest文件还有许多需要学习的地方。如果你想学习更多细节,可以阅读官方文档:http://tools.android.com/tech-docs/new-build-system/user-guide/manifest-merger
创建构建变体
Gradle可以很容易地处理复杂的构建变体。即使是创建并配置两个构建类型和两个product flavors,构建文件依然很简洁:
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"
}
}
}
本例我们创建了四个构建变体:blueDebug,blueStaging,redDebug
和redStaging
。
变体过滤器
你也可以在构建中完全忽略某个变体。这样,你就可以加快assemble
命令构建所有变体的速度,并且去掉不用执行的任务。这也可以保证在Android Studio的构建变体窗口(见图1)中不会出现这个变体的选项。
你可以在build.gradle
文件中添加如下代码来过滤掉构建变体:
android.variantFilter { variant ->
if(variant.buildType.name.equals('release')) {
variant.getFlavors().each() { flavor ->
if (flavor.name.equals('blue')) {
variant.setIgnore(true);
}
}
}
}
本例中,我们首先检查变体的构建类型是不是release
,然后我们检查该构建类型的所有flavors。getFlavors()
方法返回一个flavor数组,数组的长度等于flavor的维度数。比如,对于blueFreeDebug
变体而言,flavor数组包含blue
和free
两个flavor。本例过滤掉了blueFreeRelease
和bluePaidRelease
两个变体。运行gradlew tasks
命令,将不会看到这两个变体相关的任务。
签名配置
在你将应用发布到Google Play或其他应用市场之前,你需要用私钥进行签名。如果你有多个不同的版本,你需要为每个flavor使用不同的私钥进行签名。这就需要使用签名配置了。
android {
signingConfigs {
staging.initWith(signingConfigs.debug)
release {
storeFile file("release.keystore")
storePassword"secretpassword"
keyAlias "gradleforandroid"
keyPassword "secretpassword"
}
}
}
本例我们创建了两个不同的签名配置。
Android插件会默认配置一个名为debug
的签名配置,使用一个通用的公开密码的keystore文件,所以没有必要再为debug构建类型创建一个签名配置。
staging
配置调用了initWith()
函数,该函数通过传入的配置初始化当前配置。这就表示staging
使用了和debug
相同的签名配置。
release
配置使用storeFile
来指定keystore文件的路径,同时定义了key别名和用到的密码。
就像前面提到的,在构建文件中保存证书不是很好的设计。推荐保存在Gradle properties文件中。第七章会用一大块去讲解处理签名配置密码的一个任务。
在定义了签名配置之后,你需要将其应用到构建类型或者flavor中。构建类型和flavor都有一个名为signingConfig
的属性:
android {
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
本例使用了构建类型。如果你想为每个flavor使用不同的证书,你需要创建不同的签名配置。你可以用同样的方式定义它们:
android {
productFlavors {
blue {
signingConfig signingConfigs.release
}
}
}
通过这种方式使用签名配置会出现问题。为flavor设置签名配置时,会覆盖构建类型的签名配置。更好的方式是为每个构建类型每个flavor设置不同的配置:
android {
buildTypes {
release {
productFlavors.red.signingConfig signingConfigs.red
productFlavors.blue.signingConfig signingConfigs.blue
}
}
}