转自 yechaoa
1、前言
1.1、什么是多渠道打包
多渠道打包(Multi-channel Packaging)是指为同一个应用生成多个不同的安装包(通常是APK文件),每个安装包可以包含不同的配置、资源或元数据。
多渠道打包在移动应用开发中是一个常见的需求,特别是Android开发,因为应用市场碎片化的原因,通常一次版本更新需要上传多个甚至数十个应用商店。
1.2、为什么需要多渠道打包
主要是三点:
- 数据统计:根据渠道区分来源,统计各渠道的下载量以及覆盖率;
- 精细化运营:根据数据分析来做营销和推广,提升应用的曝光和转化;
- 厂商适配:适配不同厂商的系统API以及合规要求等;
2、实现多渠道打包
2.1、多渠道配置
2.1.1、Variant
在介绍多渠道打包配置之前,先简单介绍下Variant
概念。
Variant中文是变体
的意思,变体 (variant) 是指应用可以构建不同的版本,比如国内版和海外版、免费版与企业版等,还可以针对不同的目标API或设备类型,比如minSdk21和minSdk23、手机版和Pad版等。
变体由多个构建类型组合而成,例如debug与release,以及构建脚本中定义的产品变种。
2.1.2、productFlavors
productFlavors
中文翻译过来是产品变种
,用来定义不同变体,每个变体可以有不同的配置和资源,最终打出来的包也会不一样,通过合理配置,可以极大地提高开发和发布的灵活性。
以华为、OPPO为例,在app/build.gradle文件中配置多渠道:
ini
代码解读
复制代码
android {
namespace = "com.yechaoa.gradlex"
compileSdk = 33
defaultConfig {
applicationId = "com.yechaoa.gradlex"
// ...
}
// 多渠道打包配置
flavorDimensions += listOf("version")
productFlavors {
create("huawei") {
dimension = "version"
applicationIdSuffix = ".huawei"
versionNameSuffix = "-huawei"
versionCode = 1
buildConfigField("int", "CHANNEL_CODE", "1001")
}
create("oppo") {
dimension = "version"
applicationIdSuffix = ".oppo"
versionNameSuffix = "-oppo"
versionCode = 1
buildConfigField("int", "CHANNEL_CODE", "1002")
}
}
每次build.gradle文件中有代码改动,记得要重新Sync生效。
在productFlavors中通过create来创建渠道,并在渠道中按需配置属性参数。
这里applicationId和versionName在默认配置的基础上增加了渠道后缀。
以华为渠道为例,结果如下:
ini
代码解读
复制代码
applicationId = com.yechaoa.gradlex.huawei
versionName = GradleX-huawei
实际可以根据自己需求来,比如applicationId所有渠道保持一致,这样可以保证一个设备只有一个应用安装,反之,如果你想在一个设备上安装多个应用版本,也可以通过多渠道的方式来实现。
defaultConfig {}中的配置为应用默认配置,都可以在渠道配置productFlavors {}中进行覆写或追加。
需要注意的是,如果应用需要上传360手机助手的话,渠道配置不支持纯数字,可以增加一个字母前缀,比如A360。
ini
代码解读
复制代码
productFlavors {
create("A360") {
dimension = "version"
applicationIdSuffix = ".A360"
versionNameSuffix = "-A360"
versionCode = 1
}
}
2.1.3、flavorDimensions
flavorDimensions表示产品变种的维度
(Dimensions),是「组」的概念,同一个维度即为一个产品变种组,这里定义的是「version」,名字可以自定义,也可以有多个。
比如增加一个「environment」:
arduino
代码解读
复制代码
android {
flavorDimensions += listOf("version", "environment")
}
每个维度可以包含一个或多个产品变种,Dimensions就是用来将某个产品变种归类到特定维度中。比如把华为归类到version、OPPO归类到environment。
javascript
代码解读
复制代码
productFlavors {
create("huawei") {
dimension = "version"
}
create("oppo") {
dimension = "environment"
}
}
每个维度中的产品变种可以相互组合,生成不同的构建变体。例如,定义了两个维度environment和 version,每一个维度中各包含两个产品口味,那么最终会生成4种组合(2x2)的构建变体。
2.1.4、buildTypes
buildTypes是构建类型
,用来定义构建类型配置,比如是否开启代码混淆、是否开启调试模式等,通常包含debug和release两种类型。
javascript
代码解读
复制代码
buildTypes {
getByName("debug") {
isDebuggable = true
}
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
在多渠道配置中,构建类型与产品变种(productFlavors)一起使用,可以形成不同组合的构建变体(Variants)。
2.2、编译构建
配置多渠道之前,默认编译配置下,在/app/build/outputs/apk/
(或app/build/intermediates/apk/)目录下,只会产出一个debug包。
如下图:
output-metadata.json
文件是生成apk的元数据文件,由Android Studio 3.0开始支持。
json
代码解读
复制代码
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.yechaoa.gradlex",
"variantName": "debug",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 2,
"versionName": "2.0-default",
"outputFile": "app-debug.apk"
}
],
"elementType": "File"
}
2.2.1、构建变体组合
在配置完多渠道之后,打包方式跟平常的Run有些区别,以前是单一的构建变体,现在是多种组合的构建变体了,选择性也更多了。
在Android Studio右侧打开Gradle
面板,在build目录下可以看到具体的构建选项。
- 红色框为最全的构建方式,即所有的构建变体组合都会执行打包操作;
- 蓝色框为两两的构建组合选项,一次执行会打出2个包,比如执行assembleDebug会打出huaweiDebug包和oppoDebug包;
除了从Gradle面板选择任务执行之外,也可以使用命令行执行,例如:
bash
代码解读
复制代码
./gradlew assembleDebug
// or
./gradlew assembleDHuawei
2.2.1.1、关于组合
现在的构建配置是:
- 1个维度(flavorDimensions):version
- 2个产品变种(productFlavors):huawei、oppo
- 2个构建类型(buildTypes):debug、release
1个维度可以忽略不计,2个产品变种和2个构建类型会形成4个构建变体的组合,即2x2,这个在数学上叫「笛卡尔乘积」,即从两个集合中各取一个元素形成一个新集合中的元组,所有可能的组合都被考虑在内。
如下图:
{x,y} x {1,2} = {(x,1),(x,2),(y,1),(y,2)}
2.2.2、一次打多个
现在来执行assemble命令看看效果:
一次性打出4个包,分别是:
- app-huawei-debug.apk
- app-huawei-release-unsigned.apk
- app-oppo-debug.apk
- app-oppo-release-unsigned.apk
再来执行一个assembleDebug看看:
则分别打出两个debug包:
- app-huawei-debug.apk
- app-oppo-debug.apk
2.2.3、一次打一个
如果要单独打某一个构建变体的包,第一种方式是通过命令行来执行。
格式:
css
代码解读
复制代码
assemble+[ProductFlavor]+[BuildType]
示例:
bash
代码解读
复制代码
./gradlew assembleHuaweiDebug
第二种方式,则可以在Android Studio右上角找到Build Variants
按钮,或通过菜单栏导航到 View > Tool Windows > Build Variants
来打开Build Variants面板,在面板中选择要构建的变体。
选择之后,这时再点击Run按钮默认就是该变体编译。
2.2.4、渠道过滤
某些情况下,想在渠道构建中去掉某个渠道,分开打又太费时费力,这时候就可以创建变体过滤器
来移除它。
以oppo为例:
javascript
代码解读
复制代码
android {
...
flavorDimensions += listOf("version")
productFlavors {
create("huawei") {...}
create("oppo") {...}
}
}
androidComponents {
beforeVariants { variantBuilder ->
if (variantBuilder.productFlavors.containsAll(listOf("version" to "oppo"))) {
variantBuilder.enable = false
}
}
}
这里我们用到androidComponents,它是Android Gradle Plugin(AGP)提供的插件扩展能力。
androidComponents有3个回调,分别是beforeVariants、onVariants和finalizeDsl。
beforeVariants顾名思义,表示在创建变体之前回调,可以对变体进行一些修改操作。
通过设置variantBuilder.enable = false
就可以忽略oppo构建变体了,同时Build Variants中也不会再显示oppo变体。
3、渠道资源
在前面的渠道配置中,对applicationId和versionName加了后缀。
ini
代码解读
复制代码
productFlavors {
create("huawei") {
dimension = "version"
applicationIdSuffix = ".huawei"
versionNameSuffix = "-huawei"
versionCode = 1
buildConfigField("int", "CHANNEL_CODE", "1001")
}
}
随着市场多元化的发展,很多应用也衍生了极速版、海外版、或企业版等多种版本,对于渠道配置的定制诉求也越来越多,比如资源文件(文本、图片等)、以及代码,下面来看看这些在渠道配置中如何去做。
3.1、资源文件
我们以常见的APP名称和Icon图标为例,默认在AndroidManifest.xml文件的application标签中有如下配置:
xml
代码解读
复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
>
</application>
</manifest>
这里的android:icon和android:label分别引用的是app/src/main/目录下的mipmap图片资源和string文本资源。
app/src/main/目录为主变体目录,main相当于默认渠道,也可以新增其他渠道目录。
以「华为版」为例,app/build.gradle文件中「huawei」的渠道配置如下:
ini
代码解读
复制代码
productFlavors {
create("huawei") {
dimension = "version"
applicationIdSuffix = ".huawei"
versionNameSuffix = "-huawei"
versionCode = 1
}
}
然后在app/src/目录下新建huawei文件夹,在huawei文件夹下再新增res文件夹。
- 在res文件夹下新增mipmap文件夹(mipmap-xxhdpi),并放置华为版的应用图标ic_launcher;
- 在res文件夹下新增values文件夹,values文件夹下新建strings.xml文件,并配置华为版的应用名称;
xml
代码解读
复制代码
<resources>
<string name="app_name">GradleX华为版</string>
</resources>
这样,在构建华为渠道变体的时候,Gradle会根据构建变体来找对应的目录文件,即AndroidManifest.xml文件中的android:icon和android:label引用的资源路径会从原有的main目录变为huawei目录。
目录结构如下:
css
代码解读
复制代码
app/
└── src/
├── huawei/
│ ├── res/
│ │ ├── mipmap-xxhdpi/
│ │ │ └── ic_launcher.png
│ │ └── values/
│ │ └── strings.xml
└── main/
└── res/
├── mipmap-xxhdpi/
│ └── ic_launcher.png
└── values/
└── strings.xml
我们在Build Variants中选择huaweiDebug变体
然后点击Run看看运行效果:
可以看到标题已经从「GradleX」变为「GradleX华为版」了。
这里可以看到除了标题变了之外,其他文本并没有变化,这是因为Gradle在打包过程中进行了资源合并。
3.1.1、资源合并规则
- 渠道构建时,渠道变体(huawei)会跟主变体(main)目录下的资源进行合并;
- 如有同名配置资源,例如strings.xml文件中的app_name,则优先取渠道(huawei)配置资源进行覆盖,其他不同名的则进行合并;
- layout文件、assets文件则是替换,渠道资源(huawei)优先于主变体(main)资源;
3.2、代码文件
首先,代码文件是不支持合并的,也不支持同名。
比如先在huawei渠道下新增一个名为HuaweiUtil
的类,然后在main目录下也新增一个HuaweiUtil的类,则会出现同名异常(Redeclaration)。
其次,main作为主变体,渠道可以引用main中的代码类,main也可以引用渠道中的代码类,但是当渠道变换时,main则会出现找不到之前渠道代码类的异常,因为渠道中的代码为该渠道专属,只有在该渠道编译时才会与主变体main中的代码进行融合。
比如当我切到oppoDebug渠道变体时,在main中就无法找到HuaweiUtil类:
那如果oppo渠道想要复用huawei渠道的代码怎么办呢?这时候就用上sourceSets了。
3.3、sourceSets
sourceSets
可以为渠道指定代码路径,以及res、manifest等资源文件路径。
如果oppo渠道想要复用huawei渠道的代码,我们就可以通过sourceSets来进行如下设置。
在app/build.gradle文件中配置如下:
javascript
代码解读
复制代码
android {
productFlavors {
create("oppo") { }
}
sourceSets {
getByName("oppo"){
java.srcDirs("src/huawei/java")
}
}
}
在oppo渠道配置中,指定代码路径,这样,在oppo渠道变体进行编译时,就会把src/huawei/java目录的代码进行融合,从而实现复用huawei渠道代码的功能了。
需要注意的是,因为Gradle解析build.gradle脚本文件是自上而下的顺序,所以sourceSets代码块要写在productFlavors代码块的后面,否则会出现因为还没有解析渠道配置而找不到渠道的情况。
除了指定java目录下的代码文件之外,其他资源代码文件也是支持的,可以按需指定多个目录。
示例:
javascript
代码解读
复制代码
sourceSets {
getByName("oppo") {
java.srcDirs("src/oppo/java", "src/huawei/java")
kotlin.srcDirs("src/huawei/kotlin")
aidl.srcDirs("src/huawei/aidl")
res.srcDirs("src/huawei/res")
assets.srcDirs("src/huawei/assets")
jniLibs.srcDirs("src/huawei/jniLibs")
renderscript.srcDirs("src/huawei/rs")
manifest.srcFile("src/huawei/AndroidManifest.xml")
}
}
4、渠道依赖项
除了资源文件和代码文件之外,我们的依赖可能也会根据渠道有所不同,比如在做推送功能的时候,要接各个厂商的专有推送,就会有这样的诉求,在打华为渠道包的时候,只依赖华为的推送,而不依赖oppo的推送,也就是根据渠道来配置依赖项。
默认依赖配置:
scss
代码解读
复制代码
implementation("androidx.core:core-ktx:1.9.0")
默认test依赖配置:
scss
代码解读
复制代码
testImplementation("junit:junit:4.13.2")
这里的test就是变体,常见的还有debug和release。
当我们配置了渠道就会有对应的变体,「变体」+「依赖方式」就是渠道特有的依赖了。
以华为渠道只依赖华为推送为例:
arduino
代码解读
复制代码
"huaweiImplementation"("com.huawei.hms:push:6.11.0.300")
因为渠道依赖方式是在渠道配置之后动态生成的,在Kotlin DSL中需要用字符串来表示。
或者使用:
csharp
代码解读
复制代码
add("huaweiImplementation", "com.huawei.hms:push:6.11.0.300")
多渠道依赖方式:
- 默认依赖:implementation
- 渠道依赖:变体+Implementation,如huaweiImplementation
- 构建类型:类型+Implementation,如debugImplementation
- 组合变体:变体+类型+Implementation,如huaweiDebugImplementation
5、渠道统计
通过在打包前预置渠道信息,然后在运行时获取并上报,从而实现渠道的数据统计。
下面分别介绍两种渠道统计的方式。
5.1、meta-data
meta-data标签通常在AndroidManifest.xml文件中使用,通过键值对的方式为组件提供附加配置信息。
常见的第三方渠道统计比如友盟,会在AndroidManifest.xml中使用标签通过占位符的方式,来存储渠道信息。
配置meta-data:
xml
代码解读
复制代码
<application ...>
<meta-data android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL_NAME}"/>
...
</application>
配置占位符:
javascript
代码解读
复制代码
productFlavors {
create("huawei") {
manifestPlaceholders["UMENG_CHANNEL_NAME"] = "huawei"
}
create("oppo") {
manifestPlaceholders["UMENG_CHANNEL_NAME"] = "oppo"
}
}
通过manifestPlaceholders
来替换AndroidManifest.xml文件中value
的值,UMENG_CHANNEL_NAME要对应上。app name和app icon也可以通过这种占位符的方式来替换。
获取渠道:
typescript
代码解读
复制代码
public class ChannelInfo {
public static String getChannel(Context context) {
try {
ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
if (ai != null && ai.metaData != null) {
return ai.metaData.getString("UMENG_CHANNEL");
}
} catch (PackageManager.NameNotFoundException e) {
// Handle exception
}
return null;
}
}
在代码中通过PackageManager获取并读取meta-data信息。
5.2、BuildConfig
BuildConfig通常用来存储一些常量信息,比如版本号,或者在buildTypes中根据构建环境来定义接口请求的域名地址等,BuildConfig会在编译时生成class文件。实际上在配置多渠道信息的时候,已经默认会在BuildConfig中注入渠道信息(FLAVOR)了。
BuildConfig文件的位置在<project-root>/app/build/generated/source/buildConfig/<variant-name>/<package-name>/BuildConfig.java
获取渠道:
arduino
代码解读
复制代码
Log.wtf("yechaoa", "flavor = " + BuildConfig.FLAVOR)
如果想要自定义渠道信息,比如增加渠道号,也可以通过BuildConfig来存储。
在Gradle8.+版本中,需要开启BuildConfig功能:
ini
代码解读
复制代码
android {
buildFeatures {
buildConfig = true
}
}
或在gradle.properties文件中全局开启:
ini
代码解读
复制代码
android.defaults.buildfeatures.buildconfig=true
然后在渠道配置中加上渠道号:
javascript
代码解读
复制代码
productFlavors {
create("huawei") {
buildConfigField("int", "CHANNEL_CODE", "1001")
}
create("oppo") {
buildConfigField("int", "CHANNEL_CODE", "1002")
}
}
buildConfigField参数格式:
kotlin
代码解读
复制代码
fun buildConfigField(type: String, name: String, value: String)
type为数据格式,name为key,value为值。
配置完之后,重新Sync就会生成对应的常量了:
通过BuildConfig.CHANNEL_CODE就可以引用了。
6、Tips
当你的渠道配置越来越多的时候,app目录下的build.gradle文件就显得有些臃肿不易阅读和维护,这时候可以将配置模块化,把渠道相关配置抽成一个channel.gradle文件,然后在app/build.gradle文件中apply依赖进来,这样可以更好的管理和维护渠道项目的渠道配置,app/build.gradle文件也会清爽很多。
根目录下新建channel.gradle文件,并配置如下:
arduino
代码解读
复制代码
android {
flavorDimensions += 'version'
productFlavors {
// ...
}
sourceSets {
// ...
}
}
然后在app/build.gradle文件中依赖channel.gradle文件:
ini
代码解读
复制代码
apply(from = "../channel.gradle")
然后重新Sync即可。
7、总结
本章主要介绍了多渠道打包的配置、构建、资源管理以及数据统计等整个流程的知识点,涵盖了基本内容和实现方法,通过合理配置productFlavors、dimension和buildTypes,可以提高开发和发布的灵活性。