【Gradle-19】Android多渠道打包指南

6,737 阅读13分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

1、前言

1.1、什么是多渠道打包

多渠道打包(Multi-channel Packaging)是指为同一个应用生成多个不同的安装包(通常是APK文件),每个安装包可以包含不同的配置、资源或元数据。

多渠道打包在移动应用开发中是一个常见的需求,特别是Android开发,因为应用市场碎片化的原因,通常一次版本更新需要上传多个甚至数十个应用商店。

1.2、为什么需要多渠道打包

主要是三点:

  1. 数据统计:根据渠道区分来源,统计各渠道的下载量以及覆盖率;
  2. 精细化运营:根据数据分析来做营销和推广,提升应用的曝光和转化;
  3. 厂商适配:适配不同厂商的系统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文件中配置多渠道:

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在默认配置的基础上增加了渠道后缀。

以华为渠道为例,结果如下:

applicationId = com.yechaoa.gradlex.huawei
versionName = GradleX-huawei

实际可以根据自己需求来,比如applicationId所有渠道保持一致,这样可以保证一个设备只有一个应用安装,反之,如果你想在一个设备上安装多个应用版本,也可以通过多渠道的方式来实现。

defaultConfig {}中的配置为应用默认配置,都可以在渠道配置productFlavors {}中进行覆写或追加。

需要注意的是,如果应用需要上传360手机助手的话,渠道配置不支持纯数字,可以增加一个字母前缀,比如A360。

    productFlavors {
        create("A360") {
            dimension = "version"
            applicationIdSuffix = ".A360"
            versionNameSuffix = "-A360"
            versionCode = 1
        }
    }

2.1.3、flavorDimensions

flavorDimensions表示产品变种的维度(Dimensions),是「组」的概念,同一个维度即为一个产品变种组,这里定义的是「version」,名字可以自定义,也可以有多个。

比如增加一个「environment」:

android {
    flavorDimensions += listOf("version", "environment")
}

每个维度可以包含一个或多个产品变种,Dimensions就是用来将某个产品变种归类到特定维度中。比如把华为归类到version、OPPO归类到environment。

    productFlavors {
        create("huawei") {
            dimension = "version"
        }
        create("oppo") {
            dimension = "environment"
        }
    }

每个维度中的产品变种可以相互组合,生成不同的构建变体。例如,定义了两个维度environment和 version,每一个维度中各包含两个产品口味,那么最终会生成4种组合(2x2)的构建变体。

2.1.4、buildTypes

buildTypes是构建类型,用来定义构建类型配置,比如是否开启代码混淆、是否开启调试模式等,通常包含debug和release两种类型。

    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开始支持。

{
  "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面板选择任务执行之外,也可以使用命令行执行,例如:

./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、一次打一个

如果要单独打某一个构建变体的包,第一种方式是通过命令行来执行。

格式:

assemble+[ProductFlavor]+[BuildType]

示例:

./gradlew assembleHuaweiDebug

第二种方式,则可以在Android Studio右上角找到Build Variants按钮,或通过菜单栏导航到 View > Tool Windows > Build Variants来打开Build Variants面板,在面板中选择要构建的变体。

选择之后,这时再点击Run按钮默认就是该变体编译。

2.2.4、渠道过滤

某些情况下,想在渠道构建中去掉某个渠道,分开打又太费时费力,这时候就可以创建变体过滤器来移除它。

以oppo为例:

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加了后缀。

    productFlavors {
        create("huawei") {
            dimension = "version"
            applicationIdSuffix = ".huawei"
            versionNameSuffix = "-huawei"
            versionCode = 1
            buildConfigField("int", "CHANNEL_CODE", "1001")
        }
    }

随着市场多元化的发展,很多应用也衍生了极速版、海外版、或企业版等多种版本,对于渠道配置的定制诉求也越来越多,比如资源文件(文本、图片等)、以及代码,下面来看看这些在渠道配置中如何去做。

3.1、资源文件

我们以常见的APP名称和Icon图标为例,默认在AndroidManifest.xml文件的application标签中有如下配置:

<?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」的渠道配置如下:

    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文件,并配置华为版的应用名称;
<resources>
    <string name="app_name">GradleX华为版</string>
</resources>

这样,在构建华为渠道变体的时候,Gradle会根据构建变体来找对应的目录文件,即AndroidManifest.xml文件中的android:icon和android:label引用的资源路径会从原有的main目录变为huawei目录。

目录结构如下:

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文件中配置如下:

android {
    productFlavors { 
        create("oppo") { }
    }
    sourceSets {
        getByName("oppo"){
            java.srcDirs("src/huawei/java")
        }
    }
}

在oppo渠道配置中,指定代码路径,这样,在oppo渠道变体进行编译时,就会把src/huawei/java目录的代码进行融合,从而实现复用huawei渠道代码的功能了。

需要注意的是,因为Gradle解析build.gradle脚本文件是自上而下的顺序,所以sourceSets代码块要写在productFlavors代码块的后面,否则会出现因为还没有解析渠道配置而找不到渠道的情况。

除了指定java目录下的代码文件之外,其他资源代码文件也是支持的,可以按需指定多个目录。

示例:

    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的推送,也就是根据渠道来配置依赖项。

默认依赖配置:

implementation("androidx.core:core-ktx:1.9.0")

默认test依赖配置:

testImplementation("junit:junit:4.13.2")

这里的test就是变体,常见的还有debug和release。

当我们配置了渠道就会有对应的变体,「变体」+「依赖方式」就是渠道特有的依赖了。

以华为渠道只依赖华为推送为例:

"huaweiImplementation"("com.huawei.hms:push:6.11.0.300")

因为渠道依赖方式是在渠道配置之后动态生成的,在Kotlin DSL中需要用字符串来表示。

或者使用:

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:

<application ...>
    <meta-data android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL_NAME}"/>
    ...
</application>

配置占位符:

    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也可以通过这种占位符的方式来替换。

获取渠道:

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

获取渠道:

Log.wtf("yechaoa", "flavor = " + BuildConfig.FLAVOR)

如果想要自定义渠道信息,比如增加渠道号,也可以通过BuildConfig来存储。

在Gradle8.+版本中,需要开启BuildConfig功能:

android {
	buildFeatures {
		buildConfig = true
	}
}

或在gradle.properties文件中全局开启:

android.defaults.buildfeatures.buildconfig=true

然后在渠道配置中加上渠道号:

    productFlavors {
        create("huawei") {
            buildConfigField("int", "CHANNEL_CODE", "1001")
        }
        create("oppo") {
            buildConfigField("int", "CHANNEL_CODE", "1002")
        }
    }

buildConfigField参数格式:

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文件,并配置如下:

android {
    flavorDimensions += 'version'
    productFlavors {
        // ...
    }

    sourceSets {
        // ...
    }
}

然后在app/build.gradle文件中依赖channel.gradle文件:

apply(from = "../channel.gradle")

然后重新Sync即可。

7、总结

本章主要介绍了多渠道打包的配置、构建、资源管理以及数据统计等整个流程的知识点,涵盖了基本内容和实现方法,通过合理配置productFlavors、dimension和buildTypes,可以提高开发和发布的灵活性。

8、GitHub

github.com/yechaoa/Gra…

9、相关文档