App Bundle 优势:体积缩减、动态交付、按需模块的底层逻辑
聊 App Bundle 之前,我们先想一个问题:
为什么传统的 APK 分发方式越来越吃力?
你想想看,一个 APK 包里塞了所有屏幕密度、所有 cpu 架构、所有语言资源。用户下载时,60% 的内容其实用不上。设备是 arm64-v8a,包里却还装着 armeabi-v7a 的 so;用户只用中文,包里却有几十种语言的字符串。这不浪费流量和存储吗?
App Bundle 就是来解决这个问题的。它把应用拆成「基础包」和「配置资源包」,Google Play 在安装时只给你需要的部分。嗯,这句话说起来简单,但底层逻辑值得深挖。今天我就把三个核心优势拆开来讲。
1. 体积缩减:不是压缩,是「按需裁剪」
很多人以为 App Bundle 用了什么黑科技压缩算法。其实没有。
它的本质是:你上传一个「素材仓库」(aab),Google Play 对每台设备重新组装一个只包含必要资源的 APK。
关键数据对比:
| 维度 | 传统 APK | App Bundle 拆分 APK | 缩减比例 |
|---|---|---|---|
| so 库 | arm64-v8a + armeabi-v7a + x86 | 仅 arm64-v8a | 约 40% |
| 资源图片 | xxhdpi + xhdpi + hdpi + mdpi | 仅 xxhdpi | 约 30% |
| 语言 | 50+ 种语言 | 仅设备语言 | 视项目而定 |
| 总下载量 | 约 50 MB | 约 28 MB | 44% |
我在项目中遇到过最夸张的情况:一个新闻 App,之前打全量 APK 有 62 MB。切成 App Bundle 后,主流中文用户下载的拆分 APK 只有 29 MB。用户安装转化率直接提升了 8%。
避坑指南: 我曾经因为没配置 android:extractNativeLibs="false",导致 so 库没有被正确拆分。建议你在 build.gradle 里显式设置:
android {
bundle {
abi {
enableSplit = true
}
language {
enableSplit = true
}
density {
enableSplit = true
}
}
packagingOptions {
jniLibs { useLegacyPackaging = false }
}
}
说白了,App Bundle 不是压缩,而是「不生成你不需要的东西」。这个思路用在大型 App 上效果尤其明显。
2. 动态交付:安装时拆分 vs 运行时按需加载
体积缩减只是第一步。App Bundle 第二个核心能力是 动态交付(Dynamic Delivery)。这玩意儿分两层:
| 层级 | 触发时机 | 举例 |
|---|---|---|
| 基础拆分 | 安装时由 Google Play 完成 | abi、语言、屏幕密度 |
| 按需模块 | 运行时由应用触发 | 特色功能模块、付费内容 |
我的个人习惯是:把不常用的功能(比如 AR 扫描模块、高级滤镜包)做成 on-demand 模块。这样用户第一次安装就只有核心功能,等真正用到时再下载。
实现要点:
- 在
build.gradle里用dist:install-time表示安装时交付,dist:on-demand表示按需交付 - 运行时通过
SplitInstallManager发起下载和安装 - 模块下载后会被合并到应用的包名空间,像普通代码一样调用
举个例子,我之前做一个摄影 App。高级滤镜模块有 20 MB,但只有付费用户才用。我把它做成 on-demand 模块,用户购买后实时下载。效果很好——免费用户安装包直接少了 20 MB,付费转化率没受影响。
⚠️ 注意: on-demand 模块下载需要网络。如果用户处于弱网环境,下载可能失败。记得处理回调中的 SESSION_NOT_AVAILABLE 和 INSUFFICIENT_STORAGE 状态。
// 伪代码示意
SplitInstallManager manager = SplitInstallManagerFactory.create(context);
SplitInstallRequest request = SplitInstallRequest
.newBuilder()
.addModule("filter_module")
.build();
manager.startInstall(request)
.addOnSuccessListener(sessionId -> {
// 模块下载安装成功
})
.addOnFailureListener(e -> {
// 处理错误:重试或降级
});
3. 按需模块的底层逻辑:Play 怎么做到的?
你可能会问:运行时下载的代码,怎么动态加到 App 里?
这就要说到 Play Core 库和 Android 的 Split APK 机制了。
3.1 模块其实是一个独立的 APK 壳
每个 on-demand 模块在打包时,实际上是一个不完整的「split APK」。它没有 AndroidManifest,没有 application id,只有自己那部分代码和资源。Google Play 下载后,系统通过 SessionFileProvider 把 split APK 复制到应用的私有目录,然后由 Play Core 库调用 PackageInstaller 进行安装。
3.2 资源合并原理
安装完成后,split APK 中的资源会合并到主 APK 的 Resources 中。通过 AssetManager.addAssetPath() 的方式动态加入。所以你在代码里使用 R.id.something 时,如果资源来自模块,也能正常访问。
一个小细节: 模块里的 R 类会和主模块的 R 合并,不会冲突。但如果你在模块里用了 R.layout,确保主模块没有同名 ID,否则可能引用错误。
4. 知识框架图
下面这张图可以帮你快速回顾 App Bundle 三大优势和它们的底层关系:
5. 实战决策建议
你现在应该对底层逻辑清楚了。但什么时候用哪种拆分?我列个对照表帮你做决策:
| 场景 | 推荐策略 | 为什么 |
|---|---|---|
| 普通信息流 App | 只用 ABI / 语言 / 密度拆分 | 功能单一,按需模块收益不大 |
| 有付费功能的大型 App | 主模块 + on-demand 付费功能 | 减少免费用户安装包,转化后按需下载 |
| 游戏 App(含资源包) | 基础 + 纹理/关卡 on-demand | 首包尽量小,关卡资源边玩边下 |
| 企业/内部应用 | 全量 APK(不推荐 Bundle) | 内部分发渠道不支持 Play 拆分 |
我的经验: 别为了炫技把所有功能都做成 on-demand。模块间如果有大量共享代码,反而会增加包大小。我一般只看两个指标:
- 模块体积是否大于 5 MB
- 该功能是否只有 30% 以下的用户用
同时满足,再考虑做 on-demand。否则就留在主包里,省心。
好了,这一章的核心就这些。记住三个关键词:裁剪、拆分、按需。底层逻辑懂了,后面实践的时候你就知道每一步在干什么了。