Gradle 的 Build Variant 与多渠道打包
立泉一个现实中的常见需求,同时也是我对国内混乱的Android生态最无语的需求:
- 为不同应用商店生成同一版本号的不同渠道包,包中需带有对应商店的标识符,以统计渠道分发数据。
- 不同应用商店的审核策略不同,App应根据渠道调整部分功能状态以满足合规要求。
即iOS开发中不存在的多渠道打包,解决方案是gradle的build variant,可以用Groovy语言在Module的build.gradle中配置不同打包类型的特有逻辑,以实现用同一份代码打出符合要求的不同包。
buildType与flavor
新建Module有release和debug2个默认buildType,可以在build.gradle中为它们配置不同的签名、applicationId、应用名和其它诸如是否开启混淆、定义不同Manifest占位符等。
debug测试,如果未指定签名配置,默认使用SDK自带的debug签名文件。可以adb安装,但不能在应用商店中发布。release发布,没有默认签名配置。如果未指定签名,打包后就是未签名unsign状态,不可以被安装。
// Module的build.gradle
android {
signingConfigs {
// 定义一个签名配置,不同buildType可以使用不同签名
mySign {
storeFile file("../apqx.jks")
storePassword "123456"
keyAlias "apqx"
keyPassword "123456"
}
}
// 定义3个buildType
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// 应定义release的签名文件
signingConfig signingConfigs.mySign
}
debug {
// applicationId加后缀
// debug模式下生成同的applicationId,实现让正式版和测试版共存
applicationIdSuffix = '.debug'
// 应定义debug的签名文件
signingConfig signingConfigs.mySign
}
share {
// 继承release定义的属性,下面定义的属性将会覆盖已继承的属性
initWith release
// applicationId加后缀,使用与其它buildType不同的applicationId
applicationIdSuffix = '.share'
// 这个buildType是能以debug模式安装到设备上的
debuggable true
}
}
sync之后在工程根目录执行./gradlew tasks,列表里只有一个assemble打包task:
./gradlew tasks
Build tasks
-----------
assemble - Assemble main outputs for all the variants.
执行./gradlew assemble,gradle会逐个打包3个buildType,默认输出路径为:
{project}/{module}/build/outputs/apk/{buildType}/{module}-{buildType}.apk
但其实gradle为每个buildType(准确的说是每一个build variant,后面会提到)都生成了单独的打包task:
./gradlew assembleRelease
./gradlew assembleDebug
./gradlew assembleShare
buildType更多的是面向release和debug这2个维度,buildType中再细分打包类型,就是flavor,准确的说,每一个flavor都具有所有定义的buildType。
这里以发布到小米应用商店的mi和发布到Google Play的play为例,它们是2个flavor,每个flavor都有release、debug和share3种buildType,其中发布到小米应用商店的App名为Mi,发布到Google Play的App名为Play:
// Module的build.gradle
android {
signingConfigs {
// 定义一个签名配置,不同的buildType可以使用不同的签名
mySign {
storeFile file("../apqx.jks")
storePassword "123456"
keyAlias "apqx"
keyPassword "123456"
}
}
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "me.apqx.test"
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 定一个Manifest占位符,AndroidManifest.xml文件中用它来作App名,默认为DEF
// 不同的flavor定义不同的App名
manifestPlaceholders APP_NAME: 'DEF'
}
// 定义包含flavor的Dimiension,可以定义不同的Dimiension各自包含多个flavor,但一般只需要定义一个即可
flavorDimensions 'version'
productflavors {
// 发布到小米应用商店的flavor
mi {
// release生成目录为{project}/{module}/build/outputs/apk/mi/release/app-mi-release.apk
// debug生成目录为{project}/{module}/build/outputs/apk/mi/debug/app-mi-debug.apk
// share生成目录为{project}/{module}/build/outputs/apk/mi/share/app-mi-share.apk
dimension = 'version'
// 修改mi flavor下的应用名
manifestPlaceholders APP_NAME: 'Mi'
}
// 发布到Google Play应用商店的flavor
play {
// release生成目录为{project}/{module}/build/outputs/apk/play/release/app-play-release.apk
// debug生成目录为{project}/{module}/build/outputs/apk/play/debug/app-play-debug.apk
// share生成目录为{project}/{module}/build/outputs/apk/play/share/app-play-share.apk
dimension = 'version'
// 修改play flavor下的应用名
manifestPlaceholders APP_NAME: 'Play'
}
}
// 定义3个buildType
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// 应定义release的签名文件
signingConfig signingConfigs.mySign
}
debug {
// applicationId加后缀
// debug模式下生成不一样的applicationId,实现让正式版和测试版共存
applicationIdSuffix = '.debug'
// 应定义debug的签名文件
signingConfig signingConfigs.mySign
}
share {
// 继承release定义的属性,下面定义的属性将会覆盖已继承的属性
initWith release
// applicationId加后缀,使用与其它buildType不一样的applicationId
applicationIdSuffix = '.share'
// 这个buildType是可以以debug模式安装到设备上的
debuggable true
}
}
在AndroidManifest文件中使用flavor定义的Manifest占位符,以实现不同flavor定义不同App名:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="me.apqx.demo">
<!-- 使用Manifest占位符,会自动替换为build.gradle中定义的字符 -->
<application
android:label="${APP_NAME}">
</application>
</manifest>
sync后执行./gradlew tasks,出现了6个assemble打包task,但其实还有一些更细粒度的打包task没有显示出来。
./gradlew tasks
Build tasks
-----------
assemble - Assemble main outputs for all the variants.
assembleDebug - Assembles main outputs for all Debug variants.
assembleMi - Assembles main outputs for all Mi variants.
assemblePlay - Assembles main outputs for all Play variants.
assembleRelease - Assembles main outputs for all Release variants.
assembleShare - Assembles main outputs for all Share variants.
上面提到,mi和play都各自有release、debug和share3种buildType:
miRelease
miDebug
miShare
playRelease
playDebug
playShare
这些flavor和buildType的组合就是build variant,gradle会自动为每一个build variant生成打包task:
# 打包所有build variant
./gradlew assemble
# 打包含有release的2个build variant
./gradlew assembleRelease
# 打包含有mi的3个build variant
./gradlew assembleMi
# 打包mi的release组成的build variant
./gradlew assembleMiRelease
这些gradle task也可以在Android Studio的Gradle视图中找到,双击便可执行。

自定义输出包目录
很多公司都有自己的打包服务器,打包时从svn或git仓库拉取代码,执行gradle打包指令,把格式化包名的包输出到指定目录下:
// Module的build.gradle
android {
/**
* 过滤指定的build variant,被过滤的variant将不会出现在打包系统里,即不会生成打包task
*/
variantFilter { variant ->
// variant名
def names = variant.flavors*.name
if (names.contains("mi")) {
// gradle将会忽略符合条件的Variant
setIgnore(true)
}
}
// 默认的输出路径为 {project}/{module}/build/outputs/apk/{flavor}/{buildType}/{module}-{variant}-{buildType}.apk
android.applicationVariants.all { variant ->
// variant.name 为 {flavor}{buildType},一般类似 miDebug
// variant.versionName 为 版本号,一般类似 1.0.0
// variant.flavors.name 为 flavor名,一般类似[mi]
// variant.buildType.name 为 buildType名,一般为release或debug
variant.outputs.all {
// 定义输出的apk路径和名字
def apkName = "demo"
// 相对于默认的输出路径,向上跳2个层级,在apk目录下
// 注意,这里不再允许使用相对于工程根目录的绝对路径
outputFileName = "../../${variant.buildType.name}/${apkName}_${variant.name}_${variant.versionName}.apk"
}
}
}
多Module的build variant
如果主Module依赖一个或多个Library,主Module有多个build variant,Library也有多个build variant,那么当执行主Module的某个variant的assemble打包指令时,编译器会尝试从它所依赖的Library中寻找同样的flavor和buildType组成的variant。如果没有找到,则使用Library默认的variant。
在Android Studio的Build Variants视图中,可以修改每一个Module的默认build variant:
