多渠道打包笔记
多渠道打包是一种可以针对同一主体通过不同的配置差异,一次性输出不同结果的配置方式。灵活运用多渠道打包可以有效减少主体部分的开发和维护。而多渠道打包主要围绕productFlavors和flavorDimensions两个配置项。
风味配置productFlavors
在build.gradle的android内容中新增productFlavors即可实现多渠道配置,配置方式如下。
android{
//其他配置信息
//产品风味(各渠道配置)
productFlavors{
coffee{
//风味1配置
}
tea{
//风味2配置
}
}
}
当module未配置多渠道打包时,相当于使用一种默认风味,而每个风味在编译目标的选择上studio会默认提供debug和release两种类型,如上示例,配置了coffee和tea两个渠道(风味)之后,在编译类型上将衍生出4中类型,分别是:coffeeDebug、coffeeRelease、teaDebug、teaRelease。
风味配置项与默认项相同时会覆盖默认项(defaultConfig ),其中可以覆盖的信息包括应用包名、版本号、版本名称、sdk版本、签名配置等等。
除此之外也可以在产品风味中定义差异化的manifest配置,其中包括应用名称、图标、样式等等任何一个manifest中的属性值。
1、包名配置
android{
defaultConfig {
applicationId "com.abc.demo"
...
}
//...
//产品风味(各渠道配置)
productFlavors{
coffee{
applicationId "com.abc.test"
}
...
}
}
编译出的coffeeXXX.apk(XXX表示Debug或者Release,下同)包名将会变成com.abc.test而不是com.abc.demo
2、applicationIdSuffix(包名后缀)
android{
defaultConfig {
applicationId "com.abc.demo"
...
}
//...
//产品风味(各渠道配置)
productFlavors{
coffee{
applicationIdSuffix ".debug"
}
...
}
}
编译出的coffeeXXX.apk包名将会变成com.abc.test.debug
3、版本以及BuildConfig
versionCode、versionName、buildConfigField、minSdkVersion、targetSdkVersion
android {
//其他...
defaultConfig {
applicationId "com.abc.test"
minSdkVersion 24
targetSdkVersion 31
versionCode 1
versionName "1.0"
}
productFlavors{
coffee{
versionCode 11
versionName "11.0.0"
buildConfigField "Integer", "UI_VERSION", "1"
}
tea{
versionCode 20
versionName "20.0.1"
buildConfigField "Integer", "UI_VERSION", "2"
}
}
}
这里输出的目标apk中coffeeXXX.apk中versionCode将变成11,versionName将变成11.0.0,而该包没的BuildConfig.java中会新增一个整型(int)常量,名称为“UI_VERSION”,值为1。
BuildConfig.java类似于R.java自动生成的,包名R.java一样,位置为build/generated/source/configBuild/渠道名称XXX/res包对应的文件夹层级/BuildConfig.java
4、manifestPlaceholders(动态配置Manifest)
该配置可以在 AndroidManifest.xml 中替换的参数以达到不同应用图标、应用名称甚至其他应用属性等。比如:
productFlavors {
coffee {
versionName "coffee_${releaseTime()}"
buildConfigField "Integer", "UI_VERSION", "1"
manifestPlaceholders = [
icon : "@drawable/icon_coffee",
style : "@style/AppThemeNightCoffee",
configChanges: "orientation|keyboard|screenSize|screenLayout|locale|layoutDirection",
persistent : false,
shareUid : "",
channelVale:"coffe_app"
]
}
tea {
versionName "coffee_${releaseTime()}"
buildConfigField "Integer", "UI_VERSION", "2"
manifestPlaceholders = [
icon : "@drawable/icon_tea",
style : "@style/AppThemeLight",
configChanges: "orientation|keyboard|screenSize|screenLayout|locale|layoutDirection|uiMode",
persistent : false,
shareUid : "android.uid.system",
channelVale:"tea_app"
]
}
}
manifest中使用”${属性名}”的方式获取值,比如:
<application
android:icon="${icon}"
android:persistent="${persistent}"
android:theme="${style}"
...>
<meta-data
android:name="CHANNEL_NAME"
android:value="${channelVale}" />
<activity
android:name=".MainActivity"
android:configChanges="${configChanges}"
android:icon="${icon}"
...>
这里在application添加meta-data信息可以实现在java中获取对应渠道信息的效果。比如这里想要在java中知道当前渠道是哪个那么可以在代码中获取meta-data值就可以了。
String channelName = null;
try {
ApplicationInfo info = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
channelName = info.metaData.getString("CHANNEL_NAME");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
5、混淆配置
consumerProguardFiles主要作用于library中,包括aar或导入的library 。
作用是:负责该library被进行编译时的混淆规则,在主App 的模块下可以不用再管理各个 library 的混淆规则而直接使用各个 library 的混淆规则文件即可。如:
android{
defaultConfig {
...
consumerProguardFiles "consumer-rules.pro"
}
//产品风味(各渠道配置)
productFlavors{
teaLib{
consumerProguardFiles "rules_1.pro"
}
...
}
}
如果需要多个文件配置可以使用consumerProguardFiles,该属性是一个 List 类型,如:
consumerProguardFiles "rules_1.pro","rules_2.pro"
proguardFiles主要作用在application编译时配置需要的混淆规则 。如果在productFlavors中有配置,那么在编译过程中将忽略掉默认buildType中的规则。如:
android{
buildTypes {
release {
minifyEnabled true
proguardFiles "proguard-rules.pro"
}
//其他...
}
//产品风味(各渠道配置)
productFlavors{
tea{
proguardFiles "proguard-rules.pro","rules_1.pro"
}
...
}
}
6、签名配置
signingConfig主要进行签名信息配置,该配置一般放在buildType中的debug或者release,如:
android{
signingConfigs {
release{
storeFile file('key1.jks')
storePassword '123456'
keyAlias 'key0'
keyPassword '123456'
}
}
buildTypes {
release {
...
signingConfig signingConfigs.release
}
}
//其他信息...
}
如果需要每个渠道有不同的签名配置可以使用如下方式:
1、定义签名信息
android{
signingConfigs {
sign1{
...
}
sign2 {
...
}
}
...
}
2、在各个风味中单独指定签名
android {
productFlavors {
coffee{
signingConfig signingConfigs.sign1
}
tea{
signingConfig signingConfigs.sign2
}
...
}
...
}
以上方式均需要在同一纬度(风味纬度在后面有说明)的风味中替换,不同维度时(二维、三维等)无效
同时各个风味单独引用的方式只能更换release配置,debug需要同步更换的话可以在debug中引用release的配置,如:
buildTypes {
debug {
// 指定 debug 使用release的签名
signingConfig release.signingConfig
}
release {
...
}
}
如果需要在不同风味纬度上也可以指定不同签名,那么需要动态替换,处理方式如下:
android{
...
signingConfigs{
sign1{...}
sign2{...}
}
buildTypes {
release {
...
//默认不配置signingConfig
//signingConfig signingConfigs.sign1
}
debug{
...
signingConfig release.signingConfig
}
}
android.applicationVariants.all {
variant ->
variant.outputs.all { output ->
if (variant.flavorName.toString().startsWith("coffee")) {
...
variant.mergedFlavor.setSigningConfig signingConfigs.sign1
}else{
...
variant.mergedFlavor.setSigningConfig signingConfigs.sign2
}
...
}
}
}
applicationVariants.all是所有配置都会执行的代码,可以在执行过程中根据各个风味的不同对默认配置进行覆盖,而该代码内同样也可以覆盖release的signingConfig信息。
7、资源配置
多渠道环境下不同的渠道可能需要不同的资源(java或者res、assets等等不一样),这时候可以通过sourceSets或者在module的src下分目录的形式进行区分。
1、sourceSets配置方式
...
productFlavors{
coffee{
versionCode 11
versionName "11.0.0"
buildConfigField "Integer", "UI_VERSION", "1"
}
tea{
versionCode 20
versionName "20.0.1"
buildConfigField "Integer", "UI_VERSION", "2"
}
}
sourceSets {
main { //默认配置项
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java']
resources.srcDirs = ['src/main/resources']
aidl.srcDirs = ['src/main/aidl']
renderscript.srcDirs = ['src/renders']
res.srcDirs = ['src/main/res']
assets.srcDirs = ['src/main/assets']
jniLibs.srcDir 'src/main/jniLibs'
}
coffee.res.srcDirs = ['src/main/res-coffee'] //渠道差异配置
tea.res.srcDirs = ['src/main/res-tea'] //渠道差异配置
}
...
需要在main文件夹下新增res-coffee和res-tea目录,目录内结构跟main中res结构一致,这样配置之后相同资源会优先选择渠道内的
2、根据渠道名称构建渠道资源目录
可以直接在src目录下根据产品风味名称建立对应的资源目录,其中包含java以及res等部分,然后在各自的资源目录中处理不同风味之间的差异。
注意:
1、main中的Java文件不能和各个风味中的Java同名(包名类名一样)
2、main中res存在和其他风味中的res名称相同时会在编译是被各风味下的res覆盖
3、各风味中不存在的res则会使用main的,所以res资源必须在风味或者main中至少有一个
比如针对coffee和tea两种渠道,可以使用以下目录结构实现资源差异化
|--src
|--main
|--java
|--res
--AndroidManifest.xml
|--coffee
|--java
|--res
|--tea
|--java
|--res
在多渠道的环境下,main就是一个公共部分,其他风味都可以共享main中的资源,但是对于java资源,它不存在覆盖的情况,因此可以使用java的extends或者implements等方式进行扩展即可。
res的适配相对来说更加简单,风味资源需要覆盖main资源时只需要在相同的资源目录下新增相同名称的资源即可;如果不需要覆盖则可以随意定义
风味纬度flavorDimensions
flavorDimensions字面意思叫“风味纬度”,需要结合productFlavors一起使用,比如常见的flavorDimensions "default"表示默认一维的维度声明。
android {
flavorDimensions "default"
productFlavors {
coffee{
...
}
tea{
...
}
...
}
...
}
一维相当于在productFlavors中一个风味就一个配置,比如示例中coffee、tea都是单独的味道,可以形象的理解为一个一维坐标上的2个点。
---------coffee------------tea------------->
当flavorDimensions声明了多维时,productFlavors需要通过dimension来描述组合的方式,并且这些组合中需要全部包含flavorDimensions的所有声明。比如在coffee和tea之外新增one和two两种可以混合的口味。
android {
flavorDimensions "taste","style"
productFlavors {
coffee{
...
dimension "taste"
}
tea{
...
dimension "taste"
}
one{
dimension "style"
}
two{
dimension "style"
}
}
...
}
这里定义了taste和style两种纬度,coffe和tea关联taste纬度,而one和two关联style纬度,这样最终就可以组成coffeeOne、coffeeTwo、teaOne、teaTwo这4个基本口味,加上每种有默认的debug和release两种类型,那么就有8中口味(最终可以有8个产物)。这里的二维就相当于一个二维坐标系上的点的组合。
style
^
|
|
one coffeeOne teaOne
|
|
two coffeeTwo teaTwo
|
|
----------coffee------tea------->tast
其他三维四维以此类推
多渠道的依赖配置
针对不同的渠道,使用不同的依赖,可以在dependencies中使用Flavor名+Compile(或者api、implementation)来指定Flavor对应的所需依赖
dependencies {
//公共依赖
compileOnly files('libs/gson.jar')
...
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
//差异化依赖
coffeeApi(name: 'libA', ext: 'aar')
teaApi(name: 'libB', ext: 'aar')
teaImplementation project(':testPro')
}
也可以使用applicationVariants.all进行动态配置依赖关系(尤其是众多渠道中就一两个依赖不一样,直接配置dependencies很麻烦)
ApplicationVariant描述了应用变量属性,其中output方法可以获取到输出相关的变量信息,可以针对这些信息进行相应的打包配置。
android{
//其他配置...
flavorDimensions "default"
productFlavors {
//产品风味
}
android.applicationVariants.all { variant ->
variant.outputs.all {
if (variant.flavorName.toString().startsWith("coffee")) {
dependencies.add("${variant.flavorName}CompileOnly", fileTree(include: ['*.jar'], dir: 'libs1'))
dependencies.add("${variant.flavorName}Implementation", fileTree(include: ['*.jar'], dir: 'libs2'))
}else{
dependencies.add("${variant.flavorName}CompileOnly", fileTree(include: ['*.jar'], dir: 'libs_framework'))
}
//配置打包的apk名称(如按风味配置输出文件)
outputFileName = "app_${variant.productFlavors[0].name}_${productFlavors[0].versionName}.apk"
}
}
//其他配置...
}
注意:如果需要根据不同的渠道名称组合出apk名称,可以直接在productFlavors中根据下标获取,如
outputFileName = "${variant.productFlavors[0].name}_${variant.productFlavors[1].name}_${productFlavors[0].versionName}.apk"