Appearance
大型 App 模块化架构、SPI 机制与组件间通信
回到总览:混合开发与桥接
相关模块:跨端一致性设计:多端架构分层与设计系统 · ViewModel 架构、SavedStateHandle 与进程被杀恢复
一句话定义
大型 App 模块化(Modularization)与组件化架构是指将单体应用拆分为宿主壳工程 (App)、业务功能模块 (Feature Modules) 以及 基础公共库 (Core Modules);通过物理依赖隔离(implementation)以及 SPI (Service Provider Interface) 服务发现或路由机制,实现模块间零直接依赖的解耦通信与独立编译。
代码索引
- 对应 Lab:modularization-spi-demo
- 宿主源码入口:SpiArchitectureDemoActivity.kt
关键源码:
为什么需要
- 为什么大型团队开发不能将所有代码放在单个
app模块中?- 一句话答:单体模块会导致全量编译耗时极长、代码修改影响范围不可控、多人 Git 冲突频繁,且无法实现业务模块的独立开发与按需动态加载。
- 模块间隔离后,
feature_a模块如何调用feature_b提供的服务而不需要直接implementation project(':feature_b')?- 一句话答:通过“接口下沉(Interface Sinking)”模式:定义公共接口库
feature_b_export,feature_b实现该接口,feature_a仅依赖feature_b_export接口库,并通过 SPI (ServiceLoader) 或路由框架在运行时动态查找接口实现类。
- 一句话答:通过“接口下沉(Interface Sinking)”模式:定义公共接口库
- Gradle
api与implementation依赖指令在模块化架构中有何本质区别?- 一句话答:
implementation会隐藏依赖的传递,当被依赖模块修改时只重新编译直接依赖项,大幅缩短增量编译时间;而api会向上传递依赖,任何改动都会引发全量重新编译。
- 一句话答:
底层机制
1. 现代大型 App 三层模块化依赖拓扑
图注补充:app 模块: 仅做全局 Application 初始化与组件装配;2. 业务功能模块层 (Feature Modules)";3. 接口下沉层 (Export / API Modules)";4. 基础公共库层 (Core / Infrastructure)"。
2. 组件解耦通信三大方案对比
图注补充:SPI: SPI / ServiceLoader 容器;SPI: 1. ServiceLoader.load(IUserService.class);SPI: 2. 读取 META-INF/services/ 配置文件。
解耦方案对比表
| 方案 | 解耦机制 | 原理 | 优缺点 |
|---|---|---|---|
| SPI (ServiceLoader) | 接口与实现分离 | 读取 META-INF/services/ 接口映射 | 原生 JDK 支持,无需第三方库;但懒加载控制较弱 |
| 路由框架 (ARouter / WMRouter) | 字符串 URL 映射 | APT / KSP 编译期生成路由表,字节码插桩 | 支持页面跳转与服务发现;但依赖注解处理器与反射 |
| EventBus 事件总线 | 发布-订阅 (Pub/Sub) | 运行时内存事件派发 | 完全无接口依赖;但过度使用会导致事件链路混乱 |
代码示例与 SPI 实现
1. 接口下沉模块 (feature_user_export)
kotlin
// 放在公共接口模块,其他模块只需依赖此接口
interface IUserService {
fun getUserId(): String
fun isLogin(): Boolean
}2. 接口实现模块 (feature_user)
kotlin
// 放在具体业务模块
class UserServiceImpl : IUserService {
override fun getUserId(): String = "10086"
override fun isLogin(): Boolean = true
}在 feature_user 的 resources/META-INF/services/com.example.IUserService 文件中写入: com.example.user.UserServiceImpl
3. 调用方使用 (feature_pay)
kotlin
fun processPayment() {
// 仅依赖 IUserService 接口,无需引用 feature_user 模块
val userService = ServiceLoader.load(IUserService::class.java).firstOrNull()
if (userService?.isLogin() == true) {
println("用户已登录,开始支付: ${userService.getUserId()}")
}
}常见误配、事故后果与排障
1. 事故:模块间循环依赖导致 Gradle 编译报错
- 误配原因:
feature_a依赖了feature_b,同时feature_b又依赖了feature_a。 - 后果:Gradle 构建直接报错
Circular dependency between projects。 - 排障与修法:将公共调用的数据模型与接口提取下沉到第三个共享模块
feature_export,彻底解开循环依赖。
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| modularization-spi-demo | CLOSURE:补齐模块化 / SPI 的宿主入口、运行说明与回链 | SpiArchitectureDemoActivity.kt |
复习检查题
在 Android 模块化架构中,为什么推荐使用
implementation而不是api引入依赖?答:使用
implementation声明的依赖不会暴露给上层模块,当被依赖的模块内部发生修改时,Gradle 只需要重新编译当前模块,而不会触发上层依赖者的重新编译(避免依赖泄漏与编译链扩散),能够显著提升项目的增量编译速度。什么是“接口下沉 (Interface Sinking)”模式?它如何解决业务模块间的强耦合?
答:接口下沉是指将业务模块
feature_a需要暴露给外部的方法提取为一个独立的接口库feature_a_export。其他业务模块只依赖这个轻量的export接口库,而不直接依赖feature_a的真实实现。运行时通过 SPI 或路由框架获取接口实例,实现了模块代码在编译期的完全解耦。
速记
- 三层拓扑:App 壳装配 → Feature 业务模块 → Core 基础公共库。
- 接口下沉:模块间沟通靠 Export 接口库,严禁业务模块间直接 implementation。
- 依赖隔离:能用
implementation不用api,控制增量编译影响链。