如何在 Gradle 中定义 debug、release 和 staging 三种构建变体?

解读

国内面试中,这道题几乎必问,但面试官真正想听的不是“写三段 buildType”,而是:

  1. 你是否理解“构建变体 = buildType × productFlavor”这一核心模型;
  2. 能否把 staging 做得“像 release 一样安全,又像 debug 一样灵活”;
  3. 是否知道国内多渠道(华为、小米、OPPO、vivo)下 staging 包如何自动打签名、如何自动上传蒲公英 / fir / 企业微信;
  4. 是否能把 R8、ProGuard、资源压缩、Debuggable、LogLevel、API 域名、证书校验、埋点开关、热修插件、AAR 依赖隔离等细节一次性说清。

一句话:面试官要的是“工程级落地经验”,不是“官方文档背诵”。

知识点

  1. buildType DSL:debuggable、minifyEnabled、shrinkResources、proguardFiles、signingConfig、manifestPlaceholders、buildConfigField、resValue、isDebuggable、isProfileable、isPseudoLocalesEnabled。
  2. productFlavor 与 flavorDimensions:国内项目通常按“渠道”维度定义 dimension="store",例如 huawei、xiaomi、oppo、vivo,再与 buildType 叉乘出 4×3=12 个变体。
  3. staging 的“灰度”定位:包名后缀 .staging、图标带“测”字、可调试但关闭 Logcat 输出、接口走预发布域名、证书校验只校验 hostname 不校验 pin、埋点走测试 AppKey、热修 tinkerId 带 staging 前缀。
  4. 签名策略:debug 用 ~/.android/debug.keystore;release 与 staging 共用同一份 release.keystore,但 staging 的 signingConfig 额外配置 v1SigningEnabled false、v2SigningEnabled true,防止误上传到 Google Play。
  5. 国内合规:staging 包必须关闭 android:usesCleartextTraffic,且 manifest 中强制配置 networkSecurityConfig,只允许域名校验白名单。
  6. 构建加速:staging 开启 R8 fullMode,但关闭 obfuscation,保留行号,方便崩溃平台符号化;同时配置 gradle.properties 中 android.enableR8.fullMode=true,减少 5%~7% 包体。
  7. CI 集成:GitLab CI 中通过 gradle assembleStagingHuawei 直接产出 huaweiStaging.apk,并自动上传蒲公英,二维码推送到企业微信群;脚本里用 echo "##vso[task.setvariable variable=APK_PATH]$APK_PATH" 把路径抛给后续 stage。
  8. 变体过滤: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 上传蒲公英,返回二维码链接推送到飞书群。

拓展思考

  1. 如果 staging 需要“可调试又必须过 Google Play 预审核”,如何动态切换 isDebuggable?
    答:在 uploadToPlay 的 CI job 里,用 gradle 命令行参数 -Pstaging.debuggable=false 覆盖,脚本中读取 project.property 后反射修改 buildTypes.staging.isDebuggable,实现“同一变体、两种形态”。

  2. 国内厂商要求 64 位、分包、隐私合规双清单,staging 如何自动校验?
    答:在 staging 的 applicationVariants.all 里注册 finalizer 任务,调用 bundletool build-apks --local-testing,再用 aapt dump configurations 检查是否含 arm64-v8a;同时解析 AndroidManifest.xml 与 assets/privacy.json,校验是否含敏感权限而未在 privacy.json 声明,失败直接 abortBuild()。

  3. 未来 AGP 9.0 将强制 AAB,staging 如何产出可安装的 APK 供测试?
    答:在 staging 变体里额外开启 android.bundle.enableSplit = false,并用 bundletool build-apks --mode=universal,CI 中自动重命名 universal.apk 为 staging-universal.apk,即可继续走蒲公英分发,无需改动测试流程。