一、组件化的核心问题
随着 Android 项目规模增长,单体工程(Monolithic Project)会面临严重的工程问题:
- 编译速度:修改一行代码需要重新编译整个项目,clean build 可能需要数十分钟。
- 协作冲突:多人同时修改同一个模块,Git 冲突频繁。
- 代码边界模糊:模块之间可以直接引用对方的类,依赖关系如蛛网般纠缠。
- 复用困难:一个模块无法独立抽取给其他项目使用。
组件化(Componentization)的解决方案是:将一个庞大的 App 工程拆分为多个可独立编译、独立运行、可复用的业务模块。
组件化的四个核心能力:
- 模块隔离:模块之间通过
api和implementation控制依赖传递。 - 路由通信:模块间通过 URL Scheme 方式导航,无需持有对方的 Activity/Fragment 类引用。
- 服务发现:模块暴露 IService 接口,通过服务容器获取实现。
- 生命周期委派:Application 的生命周期事件分发给各模块。
1.1 组件化与模块化的区别
很多人混用”组件化”和”模块化”,但它们有本质区别:
- 模块化(Modularization):按照功能/职责划分代码目录,各模块之间可能存在编译期强依赖。目标是代码组织清晰。
- 组件化(Componentization):在模块化基础上,要求各业务组件之间解耦(无直接编译依赖),可以独立编译、独立运行、独立测试。目标是团队并行开发效率。
简而言之:所有组件化项目都是模块化的,但不是所有模块化项目都是组件化的。组件化 = 模块化 + 解耦 + 独立运行能力。
二、模块隔离:api vs implementation
Gradle 的 api 和 implementation 是模块隔离的基础:
// 模块 A 的 build.gradle |
implementation 的实质:编译时 B 看不到 A 的 implementation 依赖的符号(符号在 A 的编译 classpath 中但不在 B 的编译 classpath 中),但运行时这些类仍然存在(因为最终 APK 包含所有传递依赖)。这强制开发者只能通过 A 暴露的公开 API 使用 A,不能直接依赖 A 的内部实现。
Gradle 构建时的 classpath 分离:Gradle 为每个模块维护三个 classpath:
compileClasspath:编译当前模块所需的依赖(包含 api 传递,不包含 implementation 传递)runtimeClasspath:运行当前模块所需的依赖(包含所有传递)annotationProcessorPath:APT 所需的依赖
implementation 依赖只出现在 runtimeClasspath 中,不出现在 compileClasspath 中。这就是”编译期隔离”的本质。
在大型组件化工程中,通常还配置 resourcePrefix 来隔离资源名冲突:
// module_user 的 build.gradle |
这避免了不同模块定义了同名资源文件(如两个模块都有 bg_button.xml)导致的资源覆盖问题。
2.1 AndroidManifest 合并策略
多模块环境下,每个 library 模块有自己的 AndroidManifest.xml,构建时需要合并所有 Manifest。合并规则遵循优先级:
build variant Manifest > product flavor Manifest > main Manifest > library Manifest |
冲突解决通过 tools:replace 和 tools:merge 属性:
<!-- 在 app 模块的 Manifest 中解决冲突 --> |
Manifest Merger 的冲突日志级别可以通过 Gradle 配置调整:
android { |
三、组件管理器:ComponentManager 设计
一个标准的组件管理器负责组件的注册、初始化和生命周期管理:
public class ComponentManager { |
在编译期,每个模块通过 APT 自动生成注册代码:
// 自动生成的 UserComponentRegister.java(通过 APT 生成) |
3.1 组件初始化优先级
在有依赖关系的组件之间(如日志组件应在业务组件之前初始化),需要初始化优先级:
public ComponentPriority { |
四、路由表生成:APT 与 ActivityRouter 方案
路由是组件化最核心的基础设施。模块 A 需要跳转到模块 B 的页面,但不能直接 import 模块 B 的 Activity 类(这会破坏组件隔离)。
ARouter(阿里开源,https://github.com/alibaba/ARouter)是使用最广泛的方案:
// 使用 @Route 注解标记目标页面 |
4.1 APT 如何生成路由表
ARouter 的 arouter-compiler 模块在编译期扫描所有 @Route 注解:
// arouter-compiler 的核心处理逻辑(简化) |
最终生成的代码:
// ARouter$$Group$$user.java(自动生成) |
ARouter 运行时通过 path 的第一段(如 “user”)作为 group,懒加载对应的 Group 文件,然后从 atlas 中找到目标 Class,完成跳转。
4.2 ARouter 的分组懒加载机制
ARouter 不一次性加载所有路由表,而是按 group 懒加载。path 的格式是 /group/pageName,第一段即 group。Warehouse.groupsIndex 维护 group → IRouteGroup class 的映射:
// ARouter 懒加载核心 |
4.3 拦截器链
ARouter 支持全局拦截器,可在页面跳转前做登录检查等逻辑:
|
拦截器链实现的底层是责任链模式。所有 @Interceptor 注解的类在编译期被收集,运行时按 priority 排序组织为一个链表。每个拦截器的 process() 的 callback 会触发链的下一个拦截器。
4.4 路由方案的替代选择
除了 ARouter,还有几种路由方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| ARouter | APT + 反射实例化 | 功能全面、社区活跃 | 方法数多(~2000)、有反射 |
| DeepLinkDispatch | APT + 注解生成 DeepLink 表 | 与 Android Navigation 集成好 | 仅支持 Activity |
| ActivityRouter | 类 ARouter,更轻量 | 方法数少 | 功能较少 |
| Gradle Transform 方案 | Transform 扫描所有类 | 无注解、无反射 | 构建速度慢 |
五、服务发现:IService 接口模式
除了页面路由,模块之间还需要能力调用(如从用户模块获取登录状态)。ARouter 的 IService 模式:
// 在公共层(common module)定义接口 |
navigation(Class) 方法会查找该接口的 @Route 标记的实现类,通过反射实例化并返回。这样依赖关系从”编译期强依赖”变为”运行时通过接口松耦合”。
5.1 服务发现的缓存机制
ARouter 对通过 navigation(Class) 获取的服务实例做了缓存——首次调用时通过反射创建实例并缓存到 Warehouse.providers,后续调用直接返回缓存。这避免了重复反射的开销。
5.2 组件间通信方案对比
| 方案 | 通信方式 | 耦合度 | 性能 | 适用场景 |
|---|---|---|---|---|
| IService 模式 | 接口 + 路由发现 | 编译期无耦合 | 高(缓存后零开销) | 模块间能力调用 |
| EventBus | 发布-订阅 | 完全解耦 | 中(反射或索引) | 一对多事件通知 |
| BroadcastReceiver | 系统广播 | 完全解耦 | 低(跨进程) | 系统级事件 |
| ContentProvider | URI 查询 | 编译期无耦合 | 低(跨进程) | 数据共享 |
| ViewModel + SharedFlow | 观察者模式 | 编译期依赖接口 | 高 | 同一模块内通信 |
六、Application 生命周期委派
组件化后,每个业务模块都需要在 Application 启动时执行初始化(如初始化数据库、注册第三方 SDK)。但 modules 之间不能直接通信,需要一个代理机制:
// BaseApplication.java(主模块) |
各模块的 IModule 实现类通过 APT 自动收集,主模块无需逐一引用。
6.1 ContentProvider 自动初始化技巧
Android 的一个设计特性被广泛利用于组件初始化:ContentProvider 的 onCreate() 在 Application 的 onCreate() 之前被调用。Firebase、WorkManager、App Startup 等库都利用这个特性:
// 在每个模块中声明一个 Provider |
每个模块在 AndroidManifest 中声明这个 Provider,系统自动在 App 启动时调用所有 Provider 的 onCreate。这实现了模块初始化的完全自动发现——主模块无需知悉任何子模块的初始化代码。
七、工程结构最佳实践
├── app/ # 主模块(壳工程,只做拼装和 Application 初始化) |
runAlone 模式:每个业务模块在开发时可以单独作为 App 运行,通过 Gradle 配置:
// module_user 的 build.gradle |
当 runAlone = true 时,该模块就是一个独立的 Application,有自己的 AndroidManifest.xml,开发者可以独立启动和调试,无需编译其他模块。这在团队规模较大时极大地提升了开发效率。
7.1 多模块版本管理
大型组件化项目中,统一管理各模块的外部依赖版本至关重要:
// buildSrc/src/main/kotlin/Versions.kt |
更进一步,可以使用 Gradle 的 platform 和 enforcedPlatform 统一版本约束,或在 buildSrc 中使用 DependencyHandler 扩展函数统一依赖声明。
7.2 增量编译加速策略
大型组件化项目的编译速度优化:
- Gradle Build Cache:启用远程或本地缓存
org.gradle.caching=true - 配置文件(Configuration Cache):Gradle 7.0+ 启用
org.gradle.configuration-cache=true - 按需编译:只编译变更的模块及其下游依赖,使用 Gradle Enterprise 的 build scan 分析构建瓶颈
- **Kotlin Symbol Processing (KSP)**:替代 KAPT,编译速度提升 2-3 倍
- 避免传递依赖膨胀:使用
implementation代替api减少编译 classpath 大小 - 并行构建:
org.gradle.parallel=true
八、面试常问题目
Q1: implementation 和 api 的区别?为什么组件化中推荐使用 implementation?
api 依赖会传递:A 用 api 依赖 B,C 依赖 A,则 C 可以访问 B 的类。implementation 依赖不传递:C 不能访问 B 的类。组件化中使用 implementation 可以强制模块间通过接口通信,避免绕过 A 直接依赖其内部实现,实现真正的封装。同时 implementation 减少了编译时的依赖传递,显著提升增量编译速度——修改 B 时只需重新编译 B 和直接依赖 B 的 A,不需要重新编译 C。
Q2: ARouter 如何解决模块间的循环依赖问题?
模块间的循环依赖指的是 A 依赖 B 且 B 依赖 A。组件化通过提取公共层(common module)来解决:A 和 B 共同依赖 common,将需要共享的接口定义放到 common 中,通过 IService 运行时发现。这样 A 和 B 在编译期完全独立,没有相互引用。
Q3: 组件化和插件化的区别是什么?
组件化是工程结构的设计方法,所有组件最终合并到一个 APK 中,在编译期完成组装。插件化是运行时的技术,不同的业务模块可以独立打包为 APK,运行时动态加载。组件化解决的是开发和协作效率问题,插件化解决的是动态部署和热更新问题。实际项目中两者可以结合使用:在组件化基础上,对需要动态发布的组件做插件化改造。
Q4: resourcePrefix 机制是如何防止资源冲突的?
Gradle 在构建打包阶段,会将所有 library 模块和 app 模块的资源合并。如果两个模块有同名资源文件,后处理的会覆盖先处理的(取决于依赖顺序)。resourcePrefix 强制该模块的资源名以指定前缀开头,编译时通过 Lint 检查(ResourceName 规则)报错。这不是运行时机制,而是构建期约束。
Q5: 组件化中如何实现 AAR/组件级别的 ABI 隔离?
当一个组件需要独立提供 native so 库时,不同 ABI(arm64-v8a、armeabi-v7a 等)的 so 文件需要正确打包。配置策略是:在 app 模块的 build.gradle 中通过 android.defaultConfig.ndk.abiFilters 指定最终支持的 ABI,同时在各组件模块中通过 packagingOptions 控制 so 文件的去重。关键原则:最终 APK 中不能包含多个模块提供的同名 so 但不同版本——这可能导致运行时链接错误。
参考源码路径:
- ARouter:
https://github.com/alibaba/ARouter - ARouter Compiler:
arouter-compiler/src/main/java/com/alibaba/android/arouter/compiler/processor/RouteProcessor.java - DexPathList:
libcore/dalvik/src/main/java/dalvik/system/DexPathList.java - Android Gradle Plugin Dependency Resolution:
tools/base/build-system/gradle-core - Resource Prefix Lint Check:
tools/base/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ResourceNameDetector.java - Manifest Merger:
tools/base/build-system/manifest-merger - App Startup Library:
https://developer.android.com/topic/libraries/app-startup




