Android 多渠道打包与 AAB 热更新
在国内安卓应用市场(华为、小米、OPPO、vivo、应用宝等)与出海 Google Play 并行的分发场景中,同一个 React Native 版本往往会产出几十个渠道 APK 甚至同时包含 .apk 与 .aab 两种格式。
如果每个渠道包都单独编译和上传,不仅耗时,还会迅速占满原生包配额。本指南总结 Pushy 在 Android 多渠道与 AAB 场景下的最小成本接入方案。
Pushy 如何识别 Android 原生基准包
理解多渠道热更新的关键,在于服务端匹配原生基准包的三项元数据:
- 原生版本号(
packageVersion):取自android/app/build.gradle中的versionName。 - 编译时间戳(
buildTime):Gradle 构建时自动写入安装包二进制的时间戳,可用pushy parseApk或pushy parseAab查看。 - 内容指纹(
bundleHash):使用react-native-update-cli >= 2.8.5上传原生包时,CLI 会从安装包内提取内置 JS Bundle 计算sha256;当客户端集成的react-native-update >= 10.49.0时,检查更新请求会自动上报内置 Bundle 的bundleHash。只要两边指纹一致,即使重新打包导致buildTime变化,服务端也会直接放行并复用同一份差量补丁。
基于这一机制,你可以根据团队当前的打包方式选择最省力的多渠道方案。
方案一:Walle / VasDolly 动态写入渠道号(国内多渠道首选)
如果各个安卓渠道包的 JS 代码与内置静态资源完全一致,仅仅需要区分统计渠道号(如 huawei、xiaomi、oppo),强烈推荐使用基于 APK Signature Scheme v2 分块注入渠道信息的方案:
为什么这是最佳方案
这类工具不会重新编译 Java/Kotlin 或重新打包 JS Bundle,而是直接基于同一个母包 app-release.apk 在签名块中秒级生成几十个渠道包:
- 所有渠道包的
versionName、buildTime与内置index.android.bundle100% 相同; - 你只需要向 Pushy 上传一次母包(仅占用 1 个原生包配额);
- 后续针对该母包发布的任何热更新或灰度策略,会自动对全部渠道包生效。
在 JS 侧需要读取渠道号时,通过几行原生 Module 读取 Walle / VasDolly 写入的渠道字符串暴露给 JS 即可,避免把渠道名硬编码进 JS Bundle。
方案二:同时上架 Google Play(AAB)与国内渠道(APK)
许多应用需要把同一个版本同时上架 Google Play(强制要求 .aab)和国内应用市场或官网直装(要求 .apk)。
1. 在单次 Gradle 调用中同时产出 APK 与 AAB
切勿先跑一次 ./gradlew assembleRelease,再单独跑一次 ./gradlew bundleRelease——两次独立构建会触发两次写入,导致 .apk 与 .aab 带有不同的 buildTime。
请在根目录 package.json 中将两个任务放在同一次 Gradle 调用里执行:
执行打包:
同一次任务产出的两个包会共享同一份内置 Bundle 与一致的编译时间戳:
- APK:
android/app/build/outputs/apk/release/app-release.apk - AAB:
android/app/build/outputs/bundle/release/app-release.aab
你只需将其中任意一个(例如 pushy uploadAab 或 pushy uploadApk,要求 react-native-update-cli >= 2.6.0)上传到 Pushy 登记为该 versionName 的基准包,Google Play 下载的用户与 APK 直装用户即可共享同一个热更新绑定。
2. 只打 AAB,并从中提取通用 APK
如果你希望构建流水线只执行 bundleRelease,也可以使用 CLI 内置的 extractApk 命令(需 cli >= 2.6.0)直接从 .aab 提取可安装的 APK:
也可以按目标架构和语言指定分包以减小直装 APK 体积:
3. AAB 资源分割与 PNG 压缩注意事项
构建 Android Release 包(无论 APK 还是 AAB)前,请检查 android/app/build.gradle 中的两项配置:
-
必须关闭
crunchPngs:Android 构建期重压缩 PNG 会破坏增量图片补丁校验。 -
AAB 密度资源分割(
enableSplit):react-native-update >= 10.36.0:已原生适配 AAB 资源分割,无需关闭enableSplit,可直接享受 Google Play 按屏幕密度瘦身的体积优势。react-native-update < 10.36.0:若未升级 SDK,需在android.bundle.density中配置enableSplit = false,否则热更新后可能出现图片无法显示的问题(见安装配置)。
方案三:Gradle Product Flavors 多变体构建
如果工程使用了 Gradle productFlavors(例如不同渠道有不同的桌面图标、推送 AppId、AndroidManifest 占位符或原生 SDK):
情况 A:各 Flavor 的 JS 代码与内置静态资源完全一致
虽然每个 Flavor(如 assembleHuaweiRelease、assembleXiaomiRelease)会分别编译出独立的 APK 且 buildTime 不同,但只要:
react-native-update >= 10.49.0;react-native-update-cli >= 2.8.5;- 各 Flavor 的
versionName一致,且打包出的 JS Bundle 内容完全相同(不要在 Metro 打包期注入随 Flavor 变化的环境变量)。
那么所有 Flavor APK 的内容指纹(bundleHash)完全相同。你只需上传其中一个 Flavor 的 APK 作为该 versionName 的基准包,其余 Flavor 在检查更新时都会通过 bundleHash 自动匹配命中,无需逐个上传,也无需开启「忽略编译时间戳」。
情况 B:各 Flavor 的 JS 代码或内置资源存在差异
哪怕只有一行环境变量(如 process.env.CHANNEL 在打包期被内联进 JS)导致产出的 index.android.bundle 不同,不同 Flavor 的内置字节码就不再一致,无法共用同一份二进制差量补丁。此时有两种处理方式:
- 推荐改造:把编译期差异下沉到运行时读取。将渠道标识、API 环境等写入
BuildConfig或AndroidManifestmeta-data,在应用启动后由原生层传给 JS。这样所有 Flavor 重新回到「情况 A」,共用同一份 JS Bundle 和热更包。 - 独立维护:如果不同变体确实包含不同的业务模块或品牌资源(如 OEM 贴牌包),建议为每个贴牌包在 Pushy 创建独立的应用(或使用带渠道后缀的
versionName,如2.1.0-oemA),并在 CI 中通过--appId参数批量上传和发布(详见 CI/CD 自动发布热更新)。
方案四:管理后台「忽略编译时间戳」开关
对于历史遗留包(例如客户端 SDK 低于 10.49.0 无法上报 bundleHash,且多渠道已采用多次独立编译分发到线上),高级版、专业版与大客户 VIP 版可以在 Pushy 管理后台 的「应用设置」中开启忽略编译时间戳。
- 生效逻辑:服务端仅核对客户端上报的
packageVersion是否绑定了热更版本,不再拦截buildTime不一致的设备。 - 权衡说明:如果各渠道包内的基准 Bundle 实际上存在差异,增量补丁合并校验失败时客户端会自动回退到下载完整热更包(
.ppk),因此比标准单母包方案消耗更多下行流量(Pushy 套餐内不额外收取 CDN 流量费,但弱网用户体验不如几十 KB 差量补丁轻快)。新发原生版本仍建议优先升级至 SDK10.49.0+走内容指纹匹配。
常见问题
使用美团 Walle 或腾讯 VasDolly 打出 30 个渠道包,需要上传 30 次吗?
不需要。Walle 与 VasDolly 是在同一个签名母包上注入渠道信息,不会改动内置 JS Bundle 和编译时间戳。只需运行一次 pushy uploadApk 上传母包,30 个渠道包即可共享同一个热更新基准。
为什么后台已经显示了「内容指纹」,多 Flavor 包还是提示编译时间戳不一致?
内容指纹匹配需要服务端和客户端同时具备指纹:服务端通过 react-native-update-cli >= 2.8.5 上传原生包时提取指纹,而客户端需要集成 react-native-update >= 10.49.0 才会在检查更新请求中上报 bundleHash。若线上旧包内集成的 SDK 低于 10.49.0,服务端只能回退到严格比对 buildTime。排查细节参见热更新不生效与回滚排查。
Google Play 上架 AAB 后,应用内还能调用 downloadAndInstallApk 整包更新吗?
针对国内安卓渠道的直装 APK,可以在原生版本过期(expired: true)时引导下载新 APK;但对于 Google Play 渠道分发的版本,Google Play 政策禁止应用绕过商店自行下载安装 APK。建议针对 Play 渠道在检测到 expired: true 时使用 Linking.openURL("market://details?id=你的包名") 跳转商店更新页。
如何确认本地打出的 APK 和 AAB 编译时间戳是否一致?
在终端分别执行 pushy parseApk app-release.apk 和 pushy parseAab app-release.aab,对比输出的 buildTime 字段。只要通过同一次 ./gradlew clean assembleRelease bundleRelease 构建,两者的版本号与编译时间戳就会保持一致。