一、项目介绍
1. 背景与动机
在 Android 应用上线前,通常需要对同一个 APK 进行多渠道打包,以便在不同的应用市场、推广渠道或者合作伙伴分发时,能够:
-
区分来源,统计各渠道的下载量、活跃度、付费转化等指标;
-
实行差异化配置,比如某些渠道需要开启特殊功能、展示专属页面或使用不同的广告 ID;
-
提高分发灵活性,无需为每个渠道手动修改包名或资源,通过自动化流程一键生成所有渠道包。
单一 APK 的打包方式虽然简单,但无法满足上述需求。市场上一些流行的统计 SDK(如友盟、华为分析、腾讯信通院等)都依赖渠道包标识来做归因。手动管理几十甚至上百个渠道,必然容易出错、效率低下,因此自动化、多渠道打包成为 Android 发布流程中的重要一环。
2. 项目目标
本教程致力于帮助开发者从零开始,掌握:
-
渠道打包的多种实现方式:Gradle productFlavors、渠道注入脚本、第三方插件(如 Walle、Tinker Channel、PackerNg)等;
-
基于 Gradle 的自动化打包流程:如何在同一个项目中定义多个渠道、统一签名、生成渠道包并自动命名;
-
运行时读取渠道信息:将渠道 ID 嵌入 APK 后,如何在代码中准确获取;
-
渠道差异化配置:按渠道动态切换接口域名、渠道参数、打开/关闭功能等;
-
CI/CD 集成:在 Jenkins、GitLab CI、GitHub Actions 中自动执行一键打包脚本。
最终,您能够在本地或持续集成环境中,一键生成几十、上百个渠道包,并在应用启动时获知当前渠道,进行灵活业务配置。
二、相关知识
在动手之前,先了解以下核心概念和技术要点:
-
Gradle productFlavors
-
flavorDimensions
+productFlavors
:官方提供的多渠道打包方式; -
每个 flavor 对应一个渠道,可设置不同的
applicationIdSuffix
、资源替换、Manifest 占位符等;
-
-
Gradle variantFilter
-
动态过滤不需要的 buildVariant,比如只打 release 包,不生成 debug 渠道;
-
-
ManifestPlaceholders
-
在
build.gradle
中通过manifestPlaceholders
注入渠道信息到AndroidManifest.xml
;
-
-
APK 重签名与对齐
-
通过
signingConfigs
定义签名,确保所有渠道包使用同一签名; -
zipalign
保证 APK 优化对齐;
-
-
在运行时读取渠道信息
-
通过
Application
或工具类读取Meta-Data
或Assets/channelInfo
; -
第三方插件方式:Walle 等将渠道信息写入
APK
的特定区域;
-
-
脚本与 CI/CD
-
Shell / Python / Node.js 脚本调用
gradlew assemble<Flavor>Release
并批量产物重命名; -
在 Jenkins/GitLab CI 中集成,支持参数化构建。
-
三、实现思路
我们将采用Gradle productFlavors + ManifestPlaceholders 的方案,具体步骤如下:
-
配置 flavor 维度
在app/build.gradle
中定义一个名为channel
的 flavorDimension,并列出所有渠道:googlePlay
、huawei
、xiaomi
、……; -
配置每个 flavor
为每个渠道定义applicationIdSuffix
(可选)、versionNameSuffix
(可选)、manifestPlaceholders
:注入CHANNEL_ID
; -
签名配置
在build.gradle
中配置signingConfigs.release
,并在buildTypes.release
中引用; -
过滤无用 Variant
使用variantFilter
跳过 debug 渠道、测试渠道等; -
运行时读取渠道
在AndroidManifest.xml
中通过<meta-data android:name="CHANNEL_ID" android:value="${CHANNEL_ID}" />
持久化;
在Application#onCreate
读取PackageManager
获取Meta-Data
; -
打包脚本
编写一个 Shell 脚本(build_all_channels.sh
),循环调用./gradlew assemble<Channel>Release
,并将打出的 APK 重命名为app-<CHANNEL_ID>-v<versionName>.apk
; -
CI/CD 集成
在 Jenkinsfile 或.gitlab-ci.yml
中调用该脚本,实现自动化;
四、环境与依赖
-
Android Studio 4.2+ 或者 VS Code + CLI
-
Gradle 插件 7.0+
-
Kotlin DSL 或 Groovy DSL 均可(本文采用 Groovy)
-
JDK 1.8+
-
AndroidX 工程结构
五、整合代码
// 文件: app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.multichannel"
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0.0"
// 默认渠道占位符,用于 fallback
manifestPlaceholders = [CHANNEL_ID: "default"]
}
signingConfigs {
release {
storeFile file("../keystore/myapp.jks") // 根据实际路径调整
storePassword "keystore_password"
keyAlias "myapp_alias"
keyPassword "key_password"
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-DEBUG"
// debug 不对外发布,可注入特殊渠道
manifestPlaceholders = [CHANNEL_ID: "debug"]
}
release {
// release 默认无需后缀
signingConfig signingConfigs.release
// 混淆、压缩等
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// 定义渠道维度
flavorDimensions "channel"
productFlavors {
googlePlay {
dimension "channel"
// 可加后缀区别包名
applicationIdSuffix ".gp"
versionNameSuffix "-GP"
// 注入渠道 ID
manifestPlaceholders = [CHANNEL_ID: "google_play"]
}
huawei {
dimension "channel"
manifestPlaceholders = [CHANNEL_ID: "huawei"]
}
xiaomi {
dimension "channel"
manifestPlaceholders = [CHANNEL_ID: "xiaomi"]
}
vivo {
dimension "channel"
manifestPlaceholders = [CHANNEL_ID: "vivo"]
}
oppo {
dimension "channel"
manifestPlaceholders = [CHANNEL_ID: "oppo"]
}
// … 可继续添加其他渠道 …
}
// 过滤无效 Variant,例如我们不需要 debug 渠道的 assemble
variantFilter { variant ->
def names = variant.flavors*.name
if (variant.buildType.name == "debug") {
// 只保留一个 debug 变体
if (names[0] != "googlePlay") {
setIgnore(true)
}
}
}
// 统一对齐 APK
applicationVariants.all { variant ->
if (variant.buildType.name == "release") {
variant.outputs.each { output ->
def channel = variant.flavors[0].name
def version = variant.versionName
outputFileName = "app-${channel}-v${version}.apk"
}
}
}
}
// Kotlin Android 配置
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.7.20"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}
// =======================================================
// 文件: app/src/main/AndroidManifest.xml
// =======================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.multichannel">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.MultiChannel">
<!-- 渠道信息注入 -->
<meta-data
android:name="CHANNEL_ID"
android:value="${CHANNEL_ID}" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
// =======================================================
// 文件: app/src/main/java/com/example/multichannel/MyApplication.kt
// =======================================================
package com.example.multichannel
import android.app.Application
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
class MyApplication : Application() {
companion object {
var CHANNEL: String = "default"
}
override fun onCreate() {
super.onCreate()
// 运行时读取 CHANNEL_ID
try {
val ai = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
val bundle: Bundle = ai.metaData
CHANNEL = bundle.getString("CHANNEL_ID", "default")
} catch (e: Exception) {
Log.e("MyApplication", "读取 CHANNEL_ID 失败", e)
}
Log.i("MyApplication", "当前渠道: $CHANNEL")
// 可根据 CHANNEL 做不同初始化
}
}
// =======================================================
// 文件: app/src/main/java/com/example/multichannel/MainActivity.kt
// =======================================================
package com.example.multichannel
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(TextView(this).apply {
text = "当前渠道: ${MyApplication.CHANNEL}"
textSize = 24f
})
}
}
// =======================================================
// 文件: build_all_channels.sh
// =======================================================
#!/bin/bash
# 批量清理、打包、并拷贝 APK 到 output/ 目录
CHANNELS=("googlePlay" "huawei" "xiaomi" "vivo" "oppo")
OUTPUT_DIR="./channel_apks"
APK_DIR="./app/build/outputs/apk"
# 创建输出目录
rm -rf $OUTPUT_DIR
mkdir -p $OUTPUT_DIR
# 循环执行 assemble
for channel in "${CHANNELS[@]}"; do
echo ">>>>>>>>>>> 开始打包渠道: $channel <<<<<<<<<<<"
./gradlew clean
./gradlew assemble${channel^}Release # ${channel^} 首字母大写
# 复制 APK
APK_PATH=$(find $APK_DIR -type f -name "*-${channel}-v*.apk")
if [ -f "$APK_PATH" ]; then
cp "$APK_PATH" "$OUTPUT_DIR/"
echo "已拷贝: $APK_PATH → $OUTPUT_DIR/"
else
echo "未找到 $channel 渠道的 APK"
fi
done
echo "所有渠道包已输出到 $OUTPUT_DIR"
// =======================================================
// 文件: .gitlab-ci.yml (GitLab CI 示例)
// =======================================================
stages:
- build
build_apks:
stage: build
image: openjdk:8-jdk
before_script:
- apt-get update && apt-get install -y wget unzip
- wget https://services.gradle.org/distributions/gradle-7.4.2-bin.zip -P /tmp
- unzip -qq /tmp/gradle-7.4.2-bin.zip -d /opt
- export PATH=/opt/gradle-7.4.2/bin:$PATH
script:
- ./build_all_channels.sh
artifacts:
paths:
- channel_apks/
expire_in: 1 week
六、代码解读
-
app/build.gradle
-
flavorDimensions "channel"
:定义一个名为 “channel” 的维度; -
productFlavors { ... }
:枚举每个渠道,使用manifestPlaceholders
注入渠道 ID; -
variantFilter
:过滤掉不需要的变体,例如只保留一个 debug 渠道; -
applicationVariants.all { ... }
:遍历所有 release 变体,重命名 APK 为app-<channel>-v<version>.apk
;
-
-
AndroidManifest.xml
-
在
<application>
节点下通过<meta-data>
将${CHANNEL_ID}
注入到最终 APK;
-
-
MyApplication
-
在
onCreate()
中使用PackageManager.GET_META_DATA
读取CHANNEL_ID
,赋值给静态变量MyApplication.CHANNEL
; -
后续业务可以根据
CHANNEL
执行不同初始化或逻辑;
-
-
MainActivity
-
简单展示当前渠道,用于验证渠道是否正确注入;
-
-
build_all_channels.sh
-
定义所有渠道名称数组,循环调用
./gradlew assemble<Channel>Release
; -
打包完成后,从
app/build/outputs/apk
中查找对应渠道 APK 并拷贝到channel_apks/
;
-
-
CI/CD 集成示例
-
在
.gitlab-ci.yml
中,使用官方 JDK 镜像安装 Gradle,执行打包脚本,并将channel_apks/
目录作为产物保留;
-
七、项目总结与拓展
1. 总结
-
通过 Gradle 的
productFlavors
机制,实现了灵活而强大的多渠道打包,无需手动复制工程; -
使用
manifestPlaceholders
将渠道 ID 注入到 Manifest,再在运行时读取,简洁高效; -
脚本化打包,结合 CI/CD 实现一键化自动化,极大提高发布效率;
-
统一签名、自动重命名,确保所有渠道包规范可识别。
2. 拓展方向
-
第三方渠道插件
-
如 Walle 可在 APK 生成后直接写入渠道信息,无需重打包;
-
-
差异化资源替换
-
针对不同渠道,使用
src/googlePlay/res/values/strings.xml
覆盖特定资源,如应用名称、客服电话等;
-
-
动态渠道配置
-
将渠道列表、渠道参数放到远程服务器,打包时通过脚本拉取最新渠道配置;
-
-
加固与分发
-
在打包脚本中增加加固(360/梆梆加固)步骤,或上传至应用市场自动分发;
-
-
多 ABI 支持
-
结合
ndk.abiFilters
,打包多渠道多 ABI APK 或使用 App Bundle;
-