多渠道打包与 Gradle 优化:让构建更清晰、更稳定、更快
背景
一个 Android 项目从本地开发走向真实发布后,很快会遇到几个问题:测试包、预发包、正式包要连接不同环境;不同渠道可能需要不同的应用名、图标、统计标识;团队机器和 CI 上的构建速度越来越慢;构建脚本里堆满临时判断,越改越不敢动。
多渠道打包和 Gradle 优化解决的不是“能不能打包”,而是“能不能长期稳定地打包”。前者把环境、渠道、版本能力拆清楚,后者让构建过程可维护、可缓存、可诊断。
核心概念
Build Type 负责构建类型
buildTypes 通常用来区分 debug、release、staging 这类构建类型。它关心的是是否可调试、是否混淆、是否压缩资源、签名方式、日志开关等构建行为。
android {
buildTypes {
debug {
isDebuggable = true
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
一个简单判断方式:如果它影响的是“这个包怎么构建”,通常放在 buildTypes。
Product Flavor 负责产品维度
productFlavors 通常用来区分渠道、环境、品牌、客户定制等产品维度。它关心的是包名、资源、常量、渠道标识、接口域名等差异。
android {
flavorDimensions += listOf("env", "channel")
productFlavors {
create("dev") {
dimension = "env"
applicationIdSuffix = ".dev"
resValue("string", "app_name", "Demo Dev")
buildConfigField("String", "BASE_URL", "\"https://dev-api.example.com/\"")
}
create("prod") {
dimension = "env"
resValue("string", "app_name", "Demo")
buildConfigField("String", "BASE_URL", "\"https://api.example.com/\"")
}
create("xiaomi") {
dimension = "channel"
manifestPlaceholders["CHANNEL"] = "xiaomi"
}
create("huawei") {
dimension = "channel"
manifestPlaceholders["CHANNEL"] = "huawei"
}
}
}
上面配置会组合出类似 devXiaomiDebug、prodHuaweiRelease 的变体。变体越多,构建矩阵越大,所以 flavor 维度要克制,不要把所有开关都做成渠道。
多渠道打包实战
配置渠道标识
渠道标识最常见的落点是 AndroidManifest.xml 的 meta-data,再由统计 SDK 或应用代码读取。
<application>
<meta-data
android:name="APP_CHANNEL"
android:value="${CHANNEL}" />
</application>
Gradle 中通过 manifestPlaceholders 注入:
android {
flavorDimensions += "channel"
productFlavors {
create("xiaomi") {
dimension = "channel"
manifestPlaceholders["CHANNEL"] = "xiaomi"
}
create("huawei") {
dimension = "channel"
manifestPlaceholders["CHANNEL"] = "huawei"
}
}
}
代码读取时不要散落在业务层,建议封装成一个入口:
object AppMeta {
fun channel(context: Context): String {
val appInfo = context.packageManager.getApplicationInfo(
context.packageName,
PackageManager.GET_META_DATA
)
return appInfo.metaData.getString("APP_CHANNEL").orEmpty()
}
}
这样后续渠道来源从 Manifest 改成服务端下发、安装参数或其他方式时,业务代码不需要跟着大面积调整。
按环境注入接口地址
接口地址建议用 BuildConfig 注入,避免在运行时拼接一堆环境判断。
android {
flavorDimensions += "env"
productFlavors {
create("dev") {
dimension = "env"
buildConfigField("String", "BASE_URL", "\"https://dev-api.example.com/\"")
}
create("prod") {
dimension = "env"
buildConfigField("String", "BASE_URL", "\"https://api.example.com/\"")
}
}
}
网络层只依赖一个明确常量:
val retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
这种方式的好处是包一旦构建出来,环境就是确定的,不会因为运行时配置丢失而连错服务器。
按渠道覆盖资源
不同渠道如果需要不同应用名、图标或文案,可以使用 source set 覆盖资源。
app/src/main/res/values/strings.xml
app/src/xiaomi/res/values/strings.xml
app/src/huawei/res/values/strings.xml
app/src/xiaomi/res/values/strings.xml:
<resources>
<string name="app_name">Demo 小米版</string>
</resources>
资源覆盖比在代码里判断渠道更干净,也更容易被设计、测试和发布流程检查。
控制输出文件名
CI 产物最好带上应用名、版本、变体、提交信息,避免一堆 app-release.apk 混在一起。
androidComponents {
onVariants { variant ->
variant.outputs.forEach { output ->
val versionName = output.versionName.get()
output.outputFileName.set(
"demo-${variant.name}-$versionName.apk"
)
}
}
}
如果当前 Android Gradle Plugin 版本不支持某些输出 API,不要硬套旧写法。优先查对应版本文档,因为输出产物 API 在不同版本间变化比较明显。
Gradle 优化实战
开启并理解构建缓存
在 gradle.properties 中打开常用优化项:
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
android.useAndroidX=true
android.nonTransitiveRClass=true
android.nonFinalResIds=true
这些配置不是越多越好,关键是理解收益来源:
org.gradle.caching:复用可缓存任务的输出org.gradle.parallel:多模块项目并行执行任务android.nonTransitiveRClass:减少资源引用传播,降低无效编译android.nonFinalResIds:减少资源 ID 变化带来的重新编译
如果项目很小,优化收益可能不明显;如果是多模块项目,这些配置通常能明显改善增量构建体验。
避免配置阶段做重活
Gradle 构建分为配置阶段和执行阶段。很多慢构建不是编译慢,而是配置阶段脚本做了太多事情,比如读取网络、扫描大目录、动态解析一堆文件。
不推荐:
val gitHash = "git rev-parse --short HEAD".runCommand()
android.defaultConfig.buildConfigField("String", "GIT_HASH", "\"$gitHash\"")
更稳的方式是在 CI 中把信息写入环境变量,再由 Gradle 读取:
val gitHash = providers.environmentVariable("GIT_HASH").orElse("local")
android {
defaultConfig {
buildConfigField("String", "GIT_HASH", "\"${gitHash.get()}\"")
}
}
构建脚本越可预测,缓存越容易命中,CI 也越稳定。
使用 Version Catalog 管理依赖
依赖版本散落在多个 build.gradle.kts 文件中,会让升级和排查冲突变得困难。推荐用 libs.versions.toml 集中管理。
[versions]
agp = "8.6.1"
kotlin = "2.0.20"
retrofit = "2.11.0"
[libraries]
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
模块中引用:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
dependencies {
implementation(libs.retrofit)
}
这样升级依赖时能看见全局影响,也更容易做版本锁定和审查。
拆模块要围绕边界,不要只为数量
多模块能提升增量构建,但拆错了会让依赖图更复杂。比较合理的拆分方式是按稳定边界拆:
core-network:网络封装、拦截器、序列化core-database:数据库、DAO、实体映射feature-login:登录业务feature-home:首页业务design-system:通用 UI 组件和主题
模块之间尽量单向依赖,业务模块不要互相引用。公共能力下沉到 core 或 shared 层,页面能力留在 feature 层。
用构建扫描定位慢点
当构建变慢时,不要凭感觉优化。可以先跑:
./gradlew assembleDebug --scan
关注几个信息:
- 哪些任务耗时最长
- 哪些任务没有命中缓存
- 配置阶段耗时是否异常
- 是否有模块被不必要地重新编译
如果不能使用 Gradle 官方构建扫描,也可以用 --profile 生成本地报告:
./gradlew assembleDebug --profile
先定位,再优化,效率会高很多。
常见问题
渠道维度过多导致变体爆炸
两个环境、十个渠道、两个构建类型,就已经是四十个变体。维度继续增加后,IDE 同步、CI 配置、测试矩阵都会变重。
建议把真正影响安装包内容的差异放进 flavor,把运行时可配置的差异交给服务端或远程配置。
debug 和 release 行为差异太大
有些问题只在 release 出现,常见原因是混淆、资源压缩、日志开关、签名、接口环境不同。建议保留一个接近 release 的 staging 类型,用来在上线前发现问题。
buildTypes {
create("staging") {
initWith(getByName("release"))
matchingFallbacks += listOf("release")
isDebuggable = true
}
}
敏感信息写进 BuildConfig
BuildConfig 里的内容会进入安装包,不能放真正的密钥。接口域名、功能开关可以放,服务端密钥、私有 token、签名密码不要放。
签名信息建议放在本地 keystore.properties 或 CI secret 中,并确保不会提交到仓库。
val keystoreProperties = Properties().apply {
val file = rootProject.file("keystore.properties")
if (file.exists()) {
load(file.inputStream())
}
}
构建优化只改配置不看结果
优化前后最好记录基线,例如 clean build、增量 build、CI build 的耗时。否则很容易做了很多改动,却不知道是否真的有效。
./gradlew clean assembleDebug --profile
./gradlew assembleDebug --profile
前者看全量构建,后者看增量构建,两者都重要。
实战建议
多渠道打包和 Gradle 优化可以按这个顺序落地:
- 先明确环境和渠道的边界,把
buildTypes与productFlavors分清楚。 - 用
BuildConfig、manifestPlaceholders、source set 管理差异,避免业务代码到处判断渠道。 - 给 CI 产物建立稳定命名规则,保证包可追溯。
- 开启 Gradle 缓存、并行、非传递 R 类等基础优化。
- 用 Version Catalog 收敛依赖版本。
- 用
--scan或--profile找慢点,再决定是否拆模块或调整脚本。
总结
多渠道打包的核心是把差异显式化:环境归环境,渠道归渠道,构建类型归构建类型。Gradle 优化的核心是让构建可预测:少做配置阶段重活、提高缓存命中、减少无效依赖传播。
当这些基础打牢后,发布流程会更稳,CI 更容易维护,团队也能更放心地迭代构建脚本。