开发app时,通常都有好几个版本。最常见的就是有一个用来手动测试及保证质量的测试版本和一个生产版本。这些版本通常都有不同的设置。例如,测试版本的API的URL就不同于生成版本的。除此之外,还可能有一个免费的基础版本和一个付费的附带其他功能的版本。如果真的那样的话,你已经要处理四种不同的版本了:测试免费、测试收费、生产免费、生产收费。为每一种版本进行不同的配置就会变得很复杂。
gradle有一些很方便和易扩展的概念处理这些常见的问题。我们已经注意到AS会为每一个新的项目创建debug和release build类型。有个叫做product flavors的概念为管理app或library的几个版本增加了更多可能性。build类型和product flavors总是联合起来,使得处理免费、收费、测试和生产app这些场景更加容易。build type和product flavor联合体我们称之为build变体。
我们将会通过了解build type如何使得开发变得更容易以及如何充分利用它们开始这一章的学习之旅。然后我们我们将会讨论build type和product flavor的差异以及二者的用法。我们也会了解对于发布app来说必不可少的签名配置,以及如何为每一个build变体设置一个不同的签名。
这一章节里,我们将会学习以下主题:
-build types
-product flavors
-build variants
-signing configurations
在gradle的Android plugin中,build type被用来定义一个app或library应该如何被构建。每一个build type都可以指出是否应该包含debug标志、application ID应该是怎样、无用的资源是否要被清除等。你可以在buildTypes块中定义build type。一个标准的buildTypes块看起来像AS创建的一个build文件:
android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile
('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
默认的build.gradle文件为一个新的module配置一个叫做release的build type。这个build type只会禁用无用资源的移除(通过设置minifyEnabled to false)和定义默认的ProGuard配置文件的路径。这使得它直接替代开发者为产品构建启用ProGuard,而不管他们是否为之做准备。
release build type并非项目已创建的唯一的build type。默认情况下,每一个module都有一个debug build type,但是可以通过把它们纳入buildTypes块进而修改它们的配置和重写你想要改变的属性。
debug build type为了使其更易debug,有其自己默认的配置。当创建自己的build type时,可不同于默认的应用。例如,debuggable属性在debug build type中被设置成true,但是在你自己创建的build type中可设置为false。
当默认设置不够充足时,也可以创建自定义的build type。一个新创建的build type所要求的就是在buildTypes块中有一个新的对象。这有一个自定义build type叫做staging的例子:
android {
buildTypes {
staging {
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
buildConfigField "String", "API_URL",
"\"http://staging.example.com/api\""
}
}
}
staging build type为application ID定义了新的后缀,使得它不同于debug或release版本的application ID。设想你已经有了默认的build配置,加上staging build type,build type的application ID如下:
-Debug:com.package
-Release:com.package
-Staging:com.package.staging
这意味着可以在同一设备同时安装staging和release版本而不会导致冲突。staging build type也有个版本名后缀,对于区分同一设备上app的不同版本是非常有用的。buildConfigField属性使用了build配置字段(如第二章所见)为API定义了一个自定义的URL。
创建一个新的build type时,不用总是从头做起。可以初始化一个build type覆盖其他build type中的属性:
android {
buildTypes {
staging.initWith(buildTypes.debug)
staging {
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
debuggable = false
}
}
}
initWith()方法创建了一个新的build type并把一个已存的build type中的所有属性复制到其中。在新的build type对象中简单地重写属性或定义额外的属性都是有可能的。
创建一个新的build type时,gradle也会创建一个新的source set。默认情况下,source set目录被假定和build type有相同名称。但当你创建一个新的build type时该目录并非自动创建。对于一个build type,在你能使用自定义源码和资源之前要主动创建source set目录。
这就是具有标准的debug和release以及一个外加的staging build type的目录结构:
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
这些source set开启一个可能的世界。例如,你可以为特定的build type重写某些属性、增加自定义的代码和为不同的build type增加一些自定义的layout或strings。
当把Java类添加到build type中时,需记住这个进程是互斥的。也就是说如果你把CustomLogic.java添加到staging source set中时,你可能会把相同的类添加到debug和release source sets而没有添加到main source sets中。这个类将被定义两次,当你尝试build时会抛出异常。
当使用不同的source sets时,资源将会以一种特殊的方式被处理。Drawables和layouts文件将会完全重写main source set中相同名称的资源,但是values目录下的文件不会如此(例如strings.xml)。取而代之,gralde将会合并这个build type和main下的资源。
例如,如果在main source set中有个strings.xml文件,内容如下:
<resources>
<string name="app_name">TypesAndFlavorsstring>
<string name="hello_world">Hello world!string>
resources>
如果再staging build type source set中有个strings.xml,内容如下:
<resources>
<string name="app_name">TypesAndFlavors STAGINGstring>
resources>
然后,合并的strings.xml内容如下:
<resources>
<string name="app_name">TypesAndFlavors STAGINGstring>
<string name="hello_world">Hello world!string>
resources>
当你构建一个非staging build type时,最终的strings.xml文件将会是源于main source set的文件。
对于manifest文件亦是如此。如果为build type创建一个manifest文件,不需要从main source set整个的复制manifest文件;你可以添加需要的标签。Android plugin将会把manifest文件合并。
我们在本章的后边部分详细讨论合并。
每个build type都有自己的dependencies。gradle自动为每个build type创建新的dependency。如果你仅想为debug build添加一个日志框架,可以这样做:
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'
}
你可以以这种方式使用任意dependency配置联合任意的build type。
与build type被用来为同一个app或library配置几个不同的build相反,product flavors被用来为同一个app创建不同版本。典型的例子就是有免费和付费版本的app。另外一个常见的场景就是为几个用户构建具有相同功能的app的代理。这种情况在出租车业或银行app中是非常常见的,公司创建一个对于所有用户都可重用的app。唯一的改变就是主题色、logo和后台的URL。pruduct flavors极大地简化了基于同一套代码的app的不同版本的生成过程。
如果不确定是否需要创建新的build type或新的product flavor,应该问问自己是否想要为同一个app创建一个用于内部使用的新的build type,或者用于发布到Google Play的新的APK。如果你需要一个全新的、不同于已有的、需要被发布的app,product flavors可以做到。另外,也应该坚持使用build type。
创建product flavors与创建build type是非常相似的。可以创建一个新的product flavor通过把它添加到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对象出现在所有的build脚本中一样。这意味着defaultConfig和所有的product flavors都共享相同的属性。
像build type一样,product flavors也有自己的source set目录。为特定的flavor创建一个文件夹就像用flavor名称创建一个文件夹一样容易。甚至可以更近一步专门为特定的build type与flavor的组合创建一个文件夹。文件夹的名称就是build type name伴随的flavor name。例如,如果想要blue flavor的发布版本有个不同的icon,文件夹将被叫做blueRelease。联合文件的组件要比build type和product flavors文件夹有更高的优先级。
有时候,可能想要更进一步创建product flavors之间的联合。例如,客户端A和客户端B都希望他们的app有基于相同代码,但是有不同品牌的免费和收费版本。创建四种不同的flavors意味着要有好几种复杂的设置,使用没有这么做。使用flavor dimensions会以一种有效的方式组合flavors。如下:
android {
flavorDimensions "color", "price"
productFlavors {
red {
flavorDimension "color"
}
blue {
flavorDimension "color"
}
free {
flavorDimension "price"
}
paid {
flavorDimension "price"
}
}
}
一旦添加flavor dimensions,gradle希望你为每一个flavor指定一个dimension。如果忘记了的话,将会得到一个build错误和一个解释问题的信息。flavorDimensions数组定义了dimensions和它们之间的顺序,是非常重要的。当组合两个flavors时,它们可能会定义相同的属性或资源。如果发生这种情况的话,flavor dimensions的顺序决定了哪个flavor的配置重写另外一个。在上面的例子中,color dimension重写price dimension。顺序也决定了build变体的名称。
假如默认的build配置具有debug和release两种build type,在上面的例子中安装如下定义flavors,将会生成所有的build变体如下:
-blueFreeDebug and blueFreeRelease
-bluePaidDebug and bluePaidRelease
-redFreeDebug and redFreeRelease
-redPaidDebug and redPaidRelease
build变体是build type和product flavor的组合的简单结果。无论何时创建一个新的build type或product flavor,新的变体也会随之生成。例如,如果有标准的debug和release build type,并且你创建一个red和blue product flavor,将会生成如下build变体:
这是AS中Build Variants工具栏的截图。你可以在编辑器的左下角找到,或者从View|Tool Windows|Build Variants打开。该工具栏列举了所有的build变体,也可以对之进行切换。当点击Run按钮时,改变已选中的build变体将会影响到变体运行。
如果没有product flavors,变体将会仅由build type组成。不可能没有任何build type。即使你没有创建build type,Gradle的Android plugin也会为app或library自动创建一个debug build type。
Gradle的Android plugin将会为你配置的每一个build变体创建task。默认情况下,一个新的Android app具有debug和release 两种build type。所以你已经有了assembleDebug和assembleRelease独立地构建两个APK,而且assemble使用单行命令创建它们。当你添加一个新的build type,一个新的task也会被创建。当你开始添加一个flavor,全部的task都会被创建,因为每一个build type的task都会和每一个product flavor联合起来。这意味着对于有一个build type和一个flavor的简单设置,已经有三个task构建所有的变体了。
-assembleBlue:使用blue flavor配置和生成BlueRelease与BlueDebug
-assembleDebug:使用debug build type配置和为每种product flavors生成一个debug版本。
-assembleBlueDebug:组合flavor配置和build type配置,并且flavor设置重写build type中的设置。
新的task会为每种build type、product flavor以及二者的组合而创建。
由一个build type和一个或多个product flavor联合构成的build变体也有自己的source set目录。例如,由debug build type和blue flavor与free flavor创建的变体在src/blueFreeDebug/java/.有自己的目录。可以使用sourceSet块重写目录的位置,如我们在第一章看到的。
对source set的介绍增加了build进程的额外复杂性。打包之前,Gradle 的Android plugin需要合并main source set与build type source set.除此之外,library项目也提供了额外的资源,也需要被合并到一起。manifest文件亦是如此。例如,你可能在app的debug变体中需要额外的权限用于存储日志文件。你不想在main source set中声明权限因为那样可能会惊吓到潜在客户。代之,会在debug build type source set中增加一个额外的manifest文件用于声明额外的权限。
资源和manifest的优先级顺序如下:
如果资源在flavor和main source set中同时声明,flavor中的要具有更高的优先级。这种情况下,flavor source set中的而非main source set中的资源将会被打包。library项目中声明的资源总是具有最低优先级。
关于资源和manifest的合并还有很多要学习。它是相当复杂的课题,如果要详细研究的话,可能要花一整章。代之,如果想了解更多,可以阅读官方文档:
http://tools.android.com/tech-docs/new-buildsystem/
user-guide/manifest-merger。
gralde使得处理build变体的复杂性很容易。甚至当创建和配置了两个build type和两个product flavor时,build文件还是很简明:
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。每种变体都有自己的API URL和flavor color的组合。这就是blueDebug在手机上看起来的样子:
同一个app的Staging变体如下:
第一个截图展示了blueDebug变体,它使用了定义在debug build type中的URL,并使得文本颜色基于被定义在blue product flavor终端的flavor_color。第二张截图展示了redStaging,它使用了staging URL和红色文本。red staging版本也有个与众不同的app icon,因为对于staging build type,在source set中的rawable文件夹有自己的app icon图片。
可以在build中忽略某个变体。这样的话,可以使用通用的assemble命令加速构建所有变体的进程,而且task列表里不会保护没有被执行的task。这也保证了build变体不会出现在AS的build变体切换器中。
可以使用app或library下的build.gradle文件中的如下代码过滤掉变体:
android.variantFilter { variant ->
if(variant.buildType.name.equals('release')) {
variant.getFlavors().each() { flavor ->
if (flavor.name.equals('blue')) {
variant.setIgnore(true);
}
}
}
}
在这个例子中,首先检查变体的build type是否包含release名称。然后取出所有的product flavor的名称。当使用flavor而没有dimension,在flavors数组中只有有一个生产的flavor。一旦应用了flavor dimensions,flavor数组将会包含许多flavor。在例子脚本中,我们检测blue product flavor,并告知build脚本忽略这个特殊的变体。
这就是在AS的build变体切换器中的变体过滤结果:
可以看到两个blue Release变体(blueFreeRelease和bluePaidRelease)从build变体列表过滤掉了。如果运行gradlew task,将会看到所有与这两个变体相关的所有task都已不再存在了。
在Google play或其他app商店发布app前,需要用私钥对apk进行签名。如果有一个付费和收费的版本或对于不同客户的不同的app,需要用不同的key对每种flavor进行签名。也就是说签名配置迟早是会有用的。
签名配置可定义成如下:
android {
signingConfigs {
staging.initWith(signingConfigs.debug)
release {
storeFile file("release.keystore")
storePassword"secretpassword"
keyAlias "gradleforandroid"
keyPassword "secretpassword"
}
}
}
在这个例子中,我们创建了两个不同的签名配置。debug配置是被Android plugin使用一个已知密码的通用的keystore自动建立,所以必须要为这个build type创建一个签名配置。
例子中的staging配置实用initWith(),这个方法从另外一个签名配置中复制了所有属性。这意味着staging build是使用debug key而非自定的key被签名。
release配置使用storeFile指定keystore文件的路径并且定义了key的签名和密码。
正如之前所提,在build配置文件中存储证书实非良策。使用gradle属性文件倒是不错。在第七章,会有个完整的段落用于处理签名配置密码。
定义签名配置后,需要把它们应用到build type或flavors中。build type和flavor都有个叫做signConfig的属性,内如如下:
android {
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
这个例子使用build type,但如果想要为每一个创建的flavor都使用不同的证书,需要创建不同的签名配置。可使用相同方式定义:
android {
productFlavors {
blue {
signingConfig signingConfigs.release
}
}
}
可是按照这种方式使用签名配置将会导致问题。当给flavor指定一个配置时,实际上是重写build type的签名配置。代之,你想要做的是当使用flavor时,每一个build type,每一个flavor都有一个不同的key。
android {
buildTypes {
release {
productFlavors.red.signingConfig signingConfigs.red
productFlavors.blue.signingConfig signingConfigs.blue
}
}
}
这个例子展示了对于使用了release build type的red和blue flavor如何使用不同的签名配置,而不影响到debug和staging build type。
在这一章节,我们讨论了build types,product flavors以及它们所有可能的组合。这是可在任何应用中使用的强大的工具。从一个具有不同的URL和key的简单设置到共享资源代码但具有不同品牌和好几个版本的更复杂的app;build type和product flavor会使得开发更加容易。
我们也提到了签名配置和应用,也涉及了签名product flavor时常见的陷阱。
接下来,将会介绍多模块build。这非常有用当你导入代码到library或library项目中,或当你添加一个Android Wear模块到你的app中时。