Android App Bundle 结构解析:base、feature、asset_pack
讲 App Bundle 分阶段部署,你得先搞清楚它的肚子里面到底装了啥。不少人刚开始接触这玩意儿,以为就是个 zip 包,扔给 Google Play 就完事了。其实不是。
我个人习惯把 App Bundle 比作一个“乐高积木盒”——里边有必须的底座,有可选的功能模块,还有一堆可以按需下载的素材包。这三部分各自独立,但又通过 Gradle 脚本拧在一起。
三个核心模块:base、feature、asset_pack
直接上表格,一目了然:
| 模块类型 | 作用 | 是否必须 | 安装时包含 |
|---|---|---|---|
base | 核心代码、资源、Manifest、DEX | 是 | 是 |
feature | 按需动态功能(如支付模块、AR 模块) | 否 | 否,由 Play 按需分发 |
asset_pack | 大型资源包(贴图、音视频、模型) | 否 | 否,可延迟下载 |
嗯,这里要注意:base 模块是所有分发的起点。没有 base,其他模块根本挂不上去。我在项目里就见过有人把大量代码塞进 feature 模块,结果安装包反而变大——因为 base 模块必须包含所有 feature 模块的引用,那部分引用代码本身就有体积。
base 模块——整栋楼的根基
base 模块说白了就是你现在写的 app 模块。它包含:
- 主 DEX:应用启动必须的类
- 公共资源:strings、colors、layouts 等
- Manifest:AndroidManifest.xml 中的基础声明(Activity、权限等)
- 原生库:base 模块中引用的 .so 文件
但有一个坑:base 模块不能直接引用 feature 模块中的类。你得通过接口或依赖注入来解耦。我曾经在项目里直接把 feature 的代码 import 到 base 模块,结果编译不过,报了一堆循环依赖。后来老老实实用接口 + 服务定位器模式。
feature 模块——按需安装的“插件”
feature 模块是动态分发的核心。你可以在 Google Play 控制台上配置“按需安装”或“条件分发”。比如:只有用户设备语言是日语时才下载日语输入法模块。
每个 feature 模块也是一个完整 Gradle module,但有几点硬性要求:
- 必须声明
dist:module属性 - Manifest 中需包含
dist:onDemand="true"或false - 资源、代码、Manifest 都独立打包
我见过最典型的案例:某电商 App 把“直播购物”做成 feature 模块。平时不安装,只有点击直播入口时才触发下载。这样基础包体积从 60MB 降到 35MB。
// feature 模块的 build.gradle 片段
android {
dynamicFeatures = [":feature_live"]
}
// feature 模块的 AndroidManifest.xml
<dist:module
dist:onDemand="true"
dist:title="@string/module_live">
<dist:delivery>
<dist:install-time />
</dist:delivery>
</dist:module>
asset_pack 模块——懒加载的资源仓库
asset_pack 模块和 feature 模块很像,但它 不包含代码,只包含资源。最常见的是贴图包、语音包、VR 素材等。
有三个分发模式:
- install-time:应用安装时就下载——适合必含资源
- fast-follow:安装后自动下载(不阻塞)——适合启动后很快用到的资源
- on-demand:按需下载——适合用到的瞬间才下载
举个例子:我做过一个AR测距 App。核心的 AR 引擎代码放在 base 模块,但高精度 3D 模型(共 200MB)放在 asset_pack 模块,设置成 on-demand。用户第一次进入 AR 模式才触发下载,弹出一个 Loading 进度条。这样安装包只有 8MB。
⚡ 实战建议:asset_pack 模块不要放任何 .class 或 .dex 文件,否则编译会报错。如果你的资源需要动态加载,直接在 base 模块里用 PackageManager 获取拆分 APK 的路径即可。
模块间的关系与依赖
三个模块不是平级的。我把它们的关系画了一张图,你看完就懂了:
你看,base 模块在顶层,所有 feature 和 asset_pack 都挂在它下面。箭头表示“依赖关系”。feature 模块可以引用 base 模块的类和资源,但反过来不行。这保证了基础包的纯净性。
实际上线的配置技巧
最后分享几个我在线上遇到的坑:
⚠️ 我曾经...在 feature 模块里引用了另一个 feature 模块的类,编译能过,但运行时找不到那个类。因为每个 feature 模块是独立 APK,它们之间没有直接路径。解决方案:把所有 feature 共享的接口放在 base 模块里。
- 资源冲突:base 模块里定义了
R.color.red,feature 模块里也定义了同名red,运行时可能取错。我习惯在 feature 模块的资源文件名上加前缀,比如feature_live_color_red。 - ProGuard 混淆:feature 模块的混淆规则要额外注意。我一般把 feature 模块的混淆规则写在对应模块的
proguard-rules.pro里,而不是写在 base 模块的全局规则中。 - 调试时的安装:本地调试 feature 模块推荐用
bundletool构建 APK 集,然后adb install-multiple。单纯用 Android Studio 的 Run 有时会漏打 feature 模块。
说白了,理解这三个模块的本质,你才能设计出真正“瘦”而且“灵活”的 App Bundle。base 负责瘦,feature 负责动态,asset_pack 负责大资源——各司其职,分阶段部署才能落地。
🎯 我建议:不要把 base 模块做得太胖。我给自己定了个规矩:base 模块的 dex 方法数不超过 4 万(原始方法数,不算 ProGuard 压缩后)。超过的就考虑拆成 feature 模块。这样安装包永远控制在用户可接受的范围内。