如何在打包时自动替换 manifest 中的 meta-data 字段?

解读

国内 Android 项目普遍需要“一套源码 + N 个渠道包”的打法:不同渠道(华为、小米、OPPL、应用宝、字节、海外 Google Play 等)要求 manifest 里写死不同的 appkey、buglykey、友盟 key、推送 secret、服务器环境开关等 meta-data。
面试时,面试官想确认两点:

  1. 你知不知道 manifest 最终是 mergedManifest,而不是 src/main/AndroidManifest.xml;
  2. 你能否在构建阶段(CI 或本地)把“写死值”变成“可配置值”,并且不污染主代码、不出人工失误。
    回答思路必须围绕“构建脚本自动化”展开,拒绝手动改文件、拒绝运行时反射,否则会被判“不专业”。

知识点

  1. manifest merger 流程:
    每个 variant 先合并 flavor + buildType + main + aar 中的 AndroidManifest.xml,生成 intermediates/merged_manifest/…/AndroidManifest.xml,再打包。
  2. manifest merger 占位符:
    在 manifest 中写 ${CHANNEL_KEY},Gradle 在 merger 阶段会用 build.gradle 里 manifestPlaceholders 的同名键值做字符串替换。
  3. manifestPlaceholders 作用域:
    可以写在 defaultConfig、productFlavor、buildType,后者覆盖前者;支持多渠道脚本(for 循环)动态生成 flavor。
  4. 资源替换与 BuildConfig 字段:
    若 meta-data 是整型/布尔,可用 manifestPlaceholders 转字符串;也可在 manifest 中写
    android:value="@integer/umeng_key"
    然后在 build.gradle 里 resValue "integer", "umeng_key", "123456",实现资源级替换。
  5. CI 安全:
    密钥不能写仓库,通过 local.properties、gradle.properties、环境变量、加密文件(如 Tencent KMS、阿里云 KMS、GitHub secret)注入,Gradle 读取后塞入 manifestPlaceholders。
  6. AAB 场景:
    Google Play 开启“Google 签名”后,上传 AAB,manifest 中的 key 仍按上述方式注入,但上传前需确认 Play Console 的“应用签名”证书指纹与后台配置一致。
  7. 低版本 AGP 兼容:
    AGP 3.6 以下 manifestPlaceholders 对 meta-data 的 value 只能做字符串替换;AGP 7.0+ 支持任意类型,但仍建议统一字符串,避免解析异常。
  8. 验证方法:
    编译后执行
    ./gradlew :app:processReleaseManifest
    查看 intermediates/merged_manifest/…/AndroidManifest.xml,确认占位符已被替换;再反编译 apk/aab 用 aapt dump xmltree 二次确认。

答案

“我们完全在构建阶段完成替换,不碰源码。
第一步,把需要变化的 meta-data 写成占位符:
AndroidManifest.xml
<meta-data android:name="UMENG_APPKEY" android:value="${UMENG_APPKEY}"/>
<meta-data android:name="BUGLY_APPID" android:value="${BUGLY_APPID}"/>

第二步,在模块级 build.gradle 中利用 manifestPlaceholders 注入:
android {
defaultConfig {
manifestPlaceholders = [UMENG_APPKEY: ""] // 兜底空值
}
productFlavors {
huawei {
manifestPlaceholders += [
UMENG_APPKEY: project.findProperty("HUAWEI_UMENG_KEY") ?: "",
BUGLY_APPID : project.findProperty("HUAWEI_BUGLY_ID") ?: ""
]
}
xiaomi {
manifestPlaceholders += [
UMENG_APPKEY: project.findProperty("XIAOMI_UMENG_KEY") ?: "",
BUGLY_APPID : project.findProperty("XIAOMI_BUGLY_ID") ?: ""
]
}
}
}

第三步,把真实 key 放在 CI 的环境变量或 local.properties:
local.properties
HUAWEI_UMENG_KEY=123456789
HUAWEI_BUGLY_ID=abcdef

第四步,CI 脚本里解密后写入 local.properties,再执行
./gradlew :app:assembleHuaweiRelease
打包完成即自动替换,无需人工干预。

若 key 需要区分 buildType(debug/release),可在 buildType 里再次覆盖 manifestPlaceholders,实现‘一套源码、多 flavor、多类型、零手工’的 manifest 自动替换。”

拓展思考

  1. 如果 meta-data 不是字符串而是 resource 引用,如何自动化?
    答:用 resValue 在 build.gradle 中动态生成整数、布尔或字符串资源,manifest 里写 @integer/xxx 或 @string/xxx,同样可以做到 flavor 级隔离。
  2. 当渠道数量达到 200+,手写 flavor 会爆炸,怎么办?
    答:在 gradle 中读取一份 channel.json,for 循环动态生成 productFlavors,并在循环体里把每个渠道的 key 写进 manifestPlaceholders,实现“配置即渠道”。
  3. 海外版本使用 Google Play 的“应用签名”功能后,上传的 AAB 会被 Google 重新签名,如何保证后台配置的证书指纹与最终签名一致?
    答:在 CI 里把 Google 提供的“部署证书指纹”写进 Play Console 接口,自动校验;manifest 中的 key 仍按上述方式注入,不受重签名影响。
  4. 如果项目采用 Jetpack Compose + Multi-Module,manifest 分散在多个模块,如何统一管理?
    答:只在 app 模块的 manifest 中放渠道相关 meta-data,library 模块的 manifest 只声明静态组件;通过 manifestPlaceholders 集中注入,避免子模块重复占位。
  5. 未来 Google 强制 targetSdk=35+,并引入“隐私仪表板”限制读取 metadata,如何提前适配?
    答:把敏感 key 迁移到 Gradle 生成的 BuildConfig 字段或 NDK 层,通过 JNI 读取,manifest 只保留非敏感标识位,降低被系统扫描的风险。