4. Android App Bundle结构解析

讲 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 模块,结果编译不过,报了一堆循环依赖。后来老老实实用接口 + 服务定位器模式。

📌 我的习惯:在 base 模块里定义一个 FeatureProvider 接口,每个 feature 模块去实现它。这样 base 模块通过 Java 反射或 ServiceLoader 就能动态调用,不会硬依赖。

feature 模块——按需安装的“插件”

feature 模块是动态分发的核心。你可以在 Google Play 控制台上配置“按需安装”或“条件分发”。比如:只有用户设备语言是日语时才下载日语输入法模块。

每个 feature 模块也是一个完整 Gradle module,但有几点硬性要求:

  1. 必须声明 dist:module 属性
  2. Manifest 中需包含 dist:onDemand="true" 或 false
  3. 资源、代码、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 的路径即可。

模块间的关系与依赖

三个模块不是平级的。我把它们的关系画了一张图,你看完就懂了:

App Bundle 模块结构 base 模块 核心代码 + 资源 + Manifest feature 模块 按需动态功能 feature 模块 按需动态功能 asset_pack 模块 大型资源包 ⚠️ 依赖方向 feature 和 asset_pack 都依赖 base 模块 但 feature 之间、asset_pack 之间不能互相依赖

你看,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 模块。这样安装包永远控制在用户可接受的范围内。