App Bundle 架构:基础模块、特征模块、动态模块的划分与作用
做过大型 Android 项目的朋友应该都有体会——随着业务膨胀,app 模块越来越臃肿,编译时间越来越长,最终用户下载的 APK 也大得吓人。App Bundle 加动态交付,就是为了解决这些问题来的。但要想用好它,得先搞懂它的架构:基础模块、特征模块、动态模块,这三者到底怎么划分?各自承担什么角色?
这一章咱们就把它掰开揉碎,讲清楚。我刚开始接触这个体系时也走过弯路,希望下面的经验能帮你少踩几个坑。
为什么一定要拆分模块?
说白了,不拆分的话,你只能把整个 App 当一个整体来交付。用户下载 100 MB,可能 80% 的功能他根本用不上。而做了模块化拆分之后,你可以做到:
- 按需编译——改哪个模块就编译哪个,不用每次都全量构建
- 按需交付——用户只下载他需要的功能代码和资源
- 团队并行开发——每个模块可以独立迭代,互不阻塞
我见过很多团队一上来就拆了十几个模块,结果发现构建反而更慢了。原因很简单——模块划分没有分层,所有模块平铺在一起,互相引用,乱成一团。所以,理解三层模块架构是第一步。
基础模块 —— 应用的基石
基础模块(Base Module)是整个 App Bundle 的底座。它包含那些所有其他模块都离不开的东西:
- 核心框架代码:网络层、图片加载、日志、工具类等
- 公共资源:主题、通用样式、基础字符串、字体等
- 底层依赖:Dagger / Hilt、协程、AndroidX 等第三方库
- Application 类:初始化逻辑、崩溃收集、埋点基座
这里有个关键点:基础模块在用户设备上是永远存在的,它不会被按需卸载。所以,别把任何业务逻辑塞进来。我之前重构一个项目时,发现上一个团队把登录逻辑也放进了基础模块,理由是这个功能“太基础了”……结果每个模块都依赖了登录相关的类,导致后面想拆分登录模块时,牵一发动全身。嗯,这就是典型的基础模块膨胀问题。
特征模块 —— 功能的容器
特征模块(Feature Module)承载的是具体的业务功能。比如首页、搜索、个人中心、商品详情……每个都是一个特征模块。它们的特点是:
- 功能内聚——一个模块只做一件事
- 依赖基础模块——可以使用基础模块提供的能力
- 可独立开发——每个模块由专门的团队维护
特征模块的划分原则,我建议按业务功能维度来切,而不是按技术层来切。举个例子:你可能会想把所有的网络请求放在一个“网络模块”里,把所有的 UI 放在一个“UI 模块”里——这样做其实是在按技术层拆分,结果就是每个业务功能横跨多个模块,改一个需求要同时改好几个地方。更合理的做法是按业务功能切,比如“购物车模块”包含自己的 UI、ViewModel、网络请求、数据模型,它是一个完整的垂直切片。
我在项目中遇到过一个典型场景:用户反馈“个人中心”页面打开很慢。因为是独立的特征模块,我们直接针对这个模块做了懒加载优化,完全不影响其他功能。如果当时没拆分,可能就得动整个 App 的启动流程了。
动态模块 —— 按需交付的利器
动态模块(Dynamic Module)是 App Bundle 动态交付体系里的王牌。它跟特征模块最大的区别在于:动态模块可以按条件交付——用户不需要的时候,根本不会下载它。
常见的适合做成动态模块的场景:
- 低频功能:比如帮助中心、用户协议、活动页面
- 特定机型/地区:比如某地区的支付方式、某机型的专属功能
- 付费功能:用户购买后才下载的功能模块
- 大型资源包:如 AR 模型、高清壁纸、语音包
我曾经踩过一个坑:把一个动态模块里引用了基础模块的某个内部类,结果构建时没报错,但运行时直接崩溃。原因是动态模块在下载后是独立的 split APK,它跟基础模块的 class loader 是不同的。所以 动态模块只能访问基础模块的公开 API,不能依赖内部实现。从那以后,我要求团队在基础模块里给动态模块提供专门的接口层,用 @Provides 或者 @Inject 来解耦。
动态模块的几个关键配置(在 build.gradle 中):
// 动态模块的 build.gradle
plugins {
id 'com.android.dynamic-feature'
}
android {
// 必须指定分发条件
// 例如:仅在美国地区分发
// 或在用户点击"下载"时才触发安装
...
}
dependencies {
// 动态模块必须依赖基础模块
implementation project(':app')
// 可以依赖特征模块,但不建议
// 最佳实践是只依赖基础模块
}
三大模块的协作关系
理解了每个模块的职责,接下来最关键的是搞清楚它们如何协作。我画了一张架构图,帮你把这三层的关系理清楚:
从图中可以看到,基础模块在最底层,提供基础设施;特征模块在中间层,依赖基础模块来实现具体业务;动态模块在最上层,按条件交付给用户。箭头方向是依赖方向——动态模块依赖特征模块和基础模块,特征模块依赖基础模块。反过来不行。
模块划分的最佳实践
讲了这么多理论,最后总结一下我在实际项目中沉淀下来的几条原则:
| 维度 | 基础模块 | 特征模块 | 动态模块 |
|---|---|---|---|
| 核心职责 | 技术基础设施 | 业务功能实现 | 按需交付的扩展功能 |
| 是否始终交付 | ✅ 是 | ✅ 是 | ❌ 按条件 |
| 依赖关系 | 无外部依赖 | 依赖基础模块 | 依赖基础模块(+ 特征模块可选) |
| 构建影响 | 改动影响所有模块 | 改动只影响自身 | 改动只影响自身 |
另外,还有几条实战经验:
- 基础模块要瘦——只放“不变”的东西,任何可能变化或替换的能力都不要放进去。我见过一个基础模块里塞了 5 种图片加载库,说是为了兼容不同场景……结果每个特征模块都跟着膨胀。
- 特征模块要纯——一个特征模块只做一个业务功能。如果发现一个模块里既有“购物车”又有“订单详情”,那就该拆了。
- 动态模块要稳——因为动态模块是运行时下载的,网络状况、存储空间、用户权限都会影响交付成功率。所以动态模块的代码要足够健壮,能处理下载失败、安装失败等异常情况。
Application.onCreate() 里初始化,结果模块还没下载完就触发了初始化,导致空指针崩溃。正确的做法是在使用动态模块的功能时才触发安装和初始化,用 SplitInstallManager 来管理生命周期。
小结
这一章我们聊了 App Bundle 的三层模块架构:基础模块是地基,提供公共能力;特征模块是墙体,承载业务功能;动态模块是阁楼,按需搭建。三者的划分核心在于职责分离和依赖方向——基础模块在最底层,动态模块在最上层,依赖关系单向流动。