如何在 Gradle 中定义 debug、release 和 staging 三种构建变体?
解读
国内面试中,这道题几乎必问,但面试官真正想听的不是“写三段 buildType”,而是:
- 你是否理解“构建变体 = buildType × productFlavor”这一核心模型;
- 能否把 staging 做得“像 release 一样安全,又像 debug 一样灵活”;
- 是否知道国内多渠道(华为、小米、OPPO、vivo)下 staging 包如何自动打签名、如何自动上传蒲公英 / fir / 企业微信;
- 是否能把 R8、ProGuard、资源压缩、Debuggable、LogLevel、API 域名、证书校验、埋点开关、热修插件、AAR 依赖隔离等细节一次性说清。
一句话:面试官要的是“工程级落地经验”,不是“官方文档背诵”。
知识点
- buildType DSL:debuggable、minifyEnabled、shrinkResources、proguardFiles、signingConfig、manifestPlaceholders、buildConfigField、resValue、isDebuggable、isProfileable、isPseudoLocalesEnabled。
- productFlavor 与 flavorDimensions:国内项目通常按“渠道”维度定义 dimension="store",例如 huawei、xiaomi、oppo、vivo,再与 buildType 叉乘出 4×3=12 个变体。
- staging 的“灰度”定位:包名后缀 .staging、图标带“测”字、可调试但关闭 Logcat 输出、接口走预发布域名、证书校验只校验 hostname 不校验 pin、埋点走测试 AppKey、热修 tinkerId 带 staging 前缀。
- 签名策略:debug 用 ~/.android/debug.keystore;release 与 staging 共用同一份 release.keystore,但 staging 的 signingConfig 额外配置 v1SigningEnabled false、v2SigningEnabled true,防止误上传到 Google Play。
- 国内合规:staging 包必须关闭 android:usesCleartextTraffic,且 manifest 中强制配置 networkSecurityConfig,只允许域名校验白名单。
- 构建加速:staging 开启 R8 fullMode,但关闭 obfuscation,保留行号,方便崩溃平台符号化;同时配置 gradle.properties 中 android.enableR8.fullMode=true,减少 5%~7% 包体。
- CI 集成:GitLab CI 中通过 gradle assembleStagingHuawei 直接产出 huaweiStaging.apk,并自动上传蒲公英,二维码推送到企业微信群;脚本里用 echo "##vso[task.setvariable variable=APK_PATH]$APK_PATH" 把路径抛给后续 stage。
- 变体过滤:gradle 阶段使用 variantFilter 关闭无用变体,减少 60% 构建时间,例如:
variantFilter { variant -> def names = variant.flavorName + variant.buildType.name if (names.contains("vivo") && names.contains("debug")) { setIgnore(true) } }
答案
在模块级 build.gradle(Kotlin DSL 写法,Groovy 同理)中按以下步骤一次性落地:
android {
compileSdk = 34
defaultConfig {
applicationId = "com.example.myapp"
minSdk = 23
targetSdk = 34
versionCode = ciVersionCode()
versionName = ciVersionName()
}
// 1. 维度声明:国内只按“渠道”一个维度即可
flavorDimensions += "store"
productFlavors {
create("huawei") { dimension = "store" }
create("xiaomi") { dimension = "store" }
create("oppo") { dimension = "store" }
create("vivo") { dimension = "store" }
}
// 2. 三种构建类型
signingConfigs {
create("release") {
storeFile = file("../keystore/release.keystore")
storePassword = System.getenv("KEYSTORE_PWD")
keyAlias = "myapp"
keyPassword = System.getenv("KEY_PWD")
enableV1Signing = false
enableV2Signing = true
}
// staging 与 release 共用一份签名,但名字区分开,防止误操作
getByName("debug") {
// 默认 debug.keystore 即可
}
}
buildTypes {
debug {
isDebuggable = true
applicationIdSuffix = ".debug"
manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher_debug"
buildConfigField("String", "API_HOST", "\"https://test.example.com\"")
buildConfigField("boolean", "ENABLE_LOG", "true")
signingConfig = signingConfigs.getByName("debug")
}
create("staging") {
// 核心:staging 必须 isDebuggable = false,否则华为应用市场会拒绝上传
isDebuggable = false
// 但国内灰度需要调试,所以额外打开 profileable,方便 Perfetto 抓 trace
isProfileable = true
minifyEnabled = true
shrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules-staging.pro"
)
applicationIdSuffix = ".staging"
manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher_staging"
buildConfigField("String", "API_HOST", "\"https://staging.example.com\"")
buildConfigField("boolean", "ENABLE_LOG", "false")
resValue("string", "umeng_key", "\"staging_123456789\"")
signingConfig = signingConfigs.getByName("release")
// 国内合规:强制使用网络安全配置
manifestPlaceholders["networkSecurityConfig"] = "@xml/network_security_staging"
}
release {
isDebuggable = false
isProfileable = false
minifyEnabled = true
shrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules-release.pro"
)
buildConfigField("String", "API_HOST", "\"https://api.example.com\"")
buildConfigField("boolean", "ENABLE_LOG", "false")
resValue("string", "umeng_key", "\"release_987654321\"")
signingConfig = signingConfigs.getByName("release")
manifestPlaceholders["networkSecurityConfig"] = "@xml/network_security_release"
}
}
// 3. 变体过滤,CI 提速
variantFilter {
val flavor = flavors.first().name
val type = buildType.name
// 只保留 huaweiStaging、xiaomiRelease 等常用 4 个变体
val allowList = listOf(
"huaweiStaging", "xiaomiRelease",
"oppoDebug", "vivoRelease"
)
if (!allowList.contains(flavor + type.capitalize())) {
ignore = true
}
}
// 4. 输出 APK 重命名,方便 CI 收集
applicationVariants.all {
outputs.all {
val apkName = "MyApp_${flavorName}_${buildType.name}_v${versionName}_vc${versionCode}.apk"
(this as? com.android.build.gradle.internal.api.BaseVariantOutputImpl)?.outputFileName = apkName
}
}
}
配套文件:
- proguard-rules-staging.pro:关闭 obfuscation,保留行号,-keepattributes SourceFile,LineNumberTable,-keep class com.example.** { *; }
- network_security_staging.xml:只信任 staging.example.com 证书,关闭 cleartextTrafficPermitted
- CI 脚本:./gradlew assembleHuaweiStaging 后直接 curl 上传蒲公英,返回二维码链接推送到飞书群。
拓展思考
-
如果 staging 需要“可调试又必须过 Google Play 预审核”,如何动态切换 isDebuggable?
答:在 uploadToPlay 的 CI job 里,用 gradle 命令行参数 -Pstaging.debuggable=false 覆盖,脚本中读取 project.property 后反射修改 buildTypes.staging.isDebuggable,实现“同一变体、两种形态”。 -
国内厂商要求 64 位、分包、隐私合规双清单,staging 如何自动校验?
答:在 staging 的 applicationVariants.all 里注册 finalizer 任务,调用 bundletool build-apks --local-testing,再用 aapt dump configurations 检查是否含 arm64-v8a;同时解析 AndroidManifest.xml 与 assets/privacy.json,校验是否含敏感权限而未在 privacy.json 声明,失败直接 abortBuild()。 -
未来 AGP 9.0 将强制 AAB,staging 如何产出可安装的 APK 供测试?
答:在 staging 变体里额外开启 android.bundle.enableSplit = false,并用 bundletool build-apks --mode=universal,CI 中自动重命名 universal.apk 为 staging-universal.apk,即可继续走蒲公英分发,无需改动测试流程。