Android App Bundle 基础:AAB 结构与动态功能模块
做 Android 开发这么多年,我踩过最大的坑就是 APK 越管越大,用户一看到 100MB 的安装包直接扭头就走。后来 Google 推出了 Android App Bundle(以下简称 AAB),我才算真正找到了解药。这一章我们就把 AAB 的底裤扒干净,看看它到底是怎么做到「按需交付」的。
AAB 是什么?先别想得太复杂
说白了,AAB 不是最终的安装包,而是一种发布格式。你上传到 Google Play 的是 AAB,Play 商店会根据用户设备的屏幕密度、CPU 架构、语言等条件,动态生成一个只包含该设备所需内容的 APK。这样用户下载的包体积就能砍掉 30%~50%。
我记得第一次用 AAB 替换 APK 时,团队里有人担心「那我们的签名还能用吗?」其实不用担心——Google Play 会用自己的签名密钥重新签名,你只要在 Play Console 里注册好原始密钥就行。
小提示:AAB 文件本质上是一个 ZIP 包,你可以把后缀改成 .zip 然后解压看看。里面有个叫 BundleConfig.pb 的协议缓冲区文件,记录了模块的配置信息。
AAB 目录结构:拆开来看并不复杂
一个标准的 AAB 包,目录结构大概长这样:
my_app.aab/
├── base/ # 基础模块(必须有)
│ ├── dex/ # DEX 字节码
│ ├── manifest/ # AndroidManifest.xml
│ ├── res/ # 资源文件
│ ├── assets/ # 资产文件
│ ├── lib/ # 本地库(按 ABI 分目录)
│ ├── root/ # 其它文件
│ └── ...
├── feature1/ # 动态功能模块(可选)
│ ├── dex/
│ ├── manifest/
│ ├── res/
│ └── ...
├── feature2/ # 更多模块
├── BundleConfig.pb
├── bundle.pb
└── stamp.pb # 签名证书相关
看到这里你可能觉得:「也就那样嘛,跟 APK 差不多。」没错,底层的包体结构确实相似,但 AAB 多了一层「模块化」的抽象。base 模块是必选的,用户安装设备时一定会包含。而 feature 模块则是按需交付的基石。
动态功能模块:这才是 AAB 的灵魂
什么是动态功能模块?我打个比方:你开了一家餐厅,菜单上有所有菜品。但客人点餐时,你只做他选的菜,其他的菜根本不进厨房。动态功能模块就是那种「点了才做」的能力。
在 AAB 中,动态功能模块通过 <dist:module> 标签在 AndroidManifest 里声明。官方文档把它分成了三类:
| 类型 | 说明 | 典型场景 |
|---|---|---|
| 按需模块 (on-demand) | 用户可以在应用内手动下载,也可以删除 | 额外关卡包、高级功能付费解锁 |
| 条件模块 (conditional) | 根据设备条件自动安装,比如 API 版本 | 只给 Android 12+ 的设备提供新特性 |
| 即时体验模块 (instant) | 支持免安装体验,点击即玩 | 游戏试玩、应用快速预览 |
重点:所有动态模块都必须依赖于 base 模块。base 模块包含了应用的核心逻辑、主 Activity、SharedPreferences 等。动态模块不能有自己独立的 Application 类或 ContentProvider,它们运行在 base 模块的进程里。
我踩过的一个坑:模块之间不能直接调用代码
曾经我在一个项目里这么干:在 feature1 模块的 Activity 中直接 new 了 base 模块里的一个工具类。结果构建时没问题,运行时直接 ClassNotFoundException。为什么?因为动态模块是在安装后才加载的,但编译期它并不知道 base 模块里具体的实现细节。
正确的做法是:通过接口或依赖注入(比如 Dagger)进行解耦。Google 推荐的方案是使用 Play Core API 提供的 SplitInstallManager 来管理模块的安装和卸载。
Module 的定义方式
在 build.gradle 里,你需要为每个动态模块创建一个新的 module,根目录下的 settings.gradle 要包含它:
// settings.gradle
include ':app'
include ':base'
include ':feature:ar'
include ':feature:sticker'
然后在 feature/ar/build.gradle 中声明它是动态模块:
plugins {
id 'com.android.dynamic-feature'
id 'kotlin-android'
}
android {
compileSdk 34
defaultConfig {
minSdk 21
targetSdk 34
}
}
dependencies {
implementation project(':base')
}
注意这里用的插件是 com.android.dynamic-feature 而不是 com.android.application 或 com.android.library。而且必须依赖 base 模块。
模块拆分的最佳实践
说了这么多,到底哪些功能该拆成动态模块?我个人习惯遵循一个原则:不常用、体积大、低频更新。比如 AR 功能(动辄几十 MB 的模型库)、贴纸包(几百张图片)、多语言语音包等。
- 按体积拆分:单个模块超过 10MB 就考虑拆出去
- 按用户群体拆分:只有部分用户用的功能(比如专业人士才用的高级滤镜)
- 按场景拆分:只在特定地理位置或特定设备上才需要的功能
警告:千万不要把核心功能拆成动态模块。比如登录、支付、首页加载这些。如果基础模块缺失了核心功能,用户在安装时可能会遇到无法启动的严重问题。我曾经在测试环境中装过一个缺了 base 模块的版本,结果应用一打开就闪退,找了好久才定位到问题。
一张图看懂 AAB 与动态模块的关系
下面这张 SVG 图总结了整个核心逻辑:
Splits:按维度切分更精细
除了按功能模块拆分,AAB 还有一个「分片(splits)」的概念。分片不是模块,而是模块内部的切分维度。比如 base 模块的 dex 可以按屏幕密度 split,feature 模块的 lib 可以按 ABI split。Google Play 会自动组合这些分片,生成唯一的 APK。
说白了,动态功能模块是「业务逻辑层面」的拆分,而分片是「资源/库层面」的优化。两者搭配使用,效果最好。
一个常见的疑问:模块大小有没有限制?
官方没有严格限制单个模块的体积,但有一条隐形红线:按需模块的下载大小不要超过 150MB(Google Play 对 APK 大小有硬性限制)。如果你有一个 300MB 的 AR 模型包,建议拆成多个按需模块分批下载。
我曾经给一个教育 App 做过拆分:把离线课程视频拆成每节课一个动态模块。用户只下载当前要看的课,学完可以删除。这样首装体积从 200MB 降到了 35MB,应用安装转化率提升了 18%。你不妨试试这个思路。
实用技巧:在开发阶段,你可以用 Android Studio 的「Build > Build APK(s)」来生成一个模拟 AAB 压缩后效果的 APK,快速验证体积。只是要注意,本地构建不会真的应用分片规则。
总结关键点
- AAB 是发布格式,Play 商店根据设备特征生成定制 APK
- 动态功能模块需要单独声明
com.android.dynamic-feature插件 - 模块之间通过接口或依赖注入通信,不能直接引用具体类
- 按需、条件、即时三种模块类型,各自适用不同场景
- 分片(splits)与模块是正交概念,可以叠加使用