目录
  1. 1. 一、组件化的核心问题
    1. 1.1. 1.1 组件化与模块化的区别
  2. 2. 二、模块隔离:api vs implementation
    1. 2.1. 2.1 AndroidManifest 合并策略
  3. 3. 三、组件管理器:ComponentManager 设计
    1. 3.1. 3.1 组件初始化优先级
  4. 4. 四、路由表生成:APT 与 ActivityRouter 方案
    1. 4.1. 4.1 APT 如何生成路由表
    2. 4.2. 4.2 ARouter 的分组懒加载机制
    3. 4.3. 4.3 拦截器链
    4. 4.4. 4.4 路由方案的替代选择
  5. 5. 五、服务发现:IService 接口模式
    1. 5.1. 5.1 服务发现的缓存机制
    2. 5.2. 5.2 组件间通信方案对比
  6. 6. 六、Application 生命周期委派
    1. 6.1. 6.1 ContentProvider 自动初始化技巧
  7. 7. 七、工程结构最佳实践
    1. 7.1. 7.1 多模块版本管理
    2. 7.2. 7.2 增量编译加速策略
  8. 8. 八、面试常问题目
解读开源框架系列-组件化框架设计

一、组件化的核心问题

随着 Android 项目规模增长,单体工程(Monolithic Project)会面临严重的工程问题:

  1. 编译速度:修改一行代码需要重新编译整个项目,clean build 可能需要数十分钟。
  2. 协作冲突:多人同时修改同一个模块,Git 冲突频繁。
  3. 代码边界模糊:模块之间可以直接引用对方的类,依赖关系如蛛网般纠缠。
  4. 复用困难:一个模块无法独立抽取给其他项目使用。

组件化(Componentization)的解决方案是:将一个庞大的 App 工程拆分为多个可独立编译、独立运行、可复用的业务模块

组件化的四个核心能力:

  • 模块隔离:模块之间通过 apiimplementation 控制依赖传递。
  • 路由通信:模块间通过 URL Scheme 方式导航,无需持有对方的 Activity/Fragment 类引用。
  • 服务发现:模块暴露 IService 接口,通过服务容器获取实现。
  • 生命周期委派:Application 的生命周期事件分发给各模块。

1.1 组件化与模块化的区别

很多人混用”组件化”和”模块化”,但它们有本质区别:

  • 模块化(Modularization):按照功能/职责划分代码目录,各模块之间可能存在编译期强依赖。目标是代码组织清晰。
  • 组件化(Componentization):在模块化基础上,要求各业务组件之间解耦(无直接编译依赖),可以独立编译、独立运行、独立测试。目标是团队并行开发效率。

简而言之:所有组件化项目都是模块化的,但不是所有模块化项目都是组件化的。组件化 = 模块化 + 解耦 + 独立运行能力。

二、模块隔离:api vs implementation

Gradle 的 apiimplementation 是模块隔离的基础:

// 模块 A 的 build.gradle
dependencies {
// implementation:依赖不传递,只有 A 内部可用
implementation 'com.squareup.okhttp3:okhttp:4.9.0'

// api:依赖会传递给依赖 A 的模块
api 'com.google.code.gson:gson:2.8.9'
}

// 模块 B 依赖模块 A
dependencies {
implementation project(':module_a')
// B 可以使用 gson(因为 A 用了 api)
// B 不能直接使用 okhttp(因为 A 用了 implementation)
}

implementation 的实质:编译时 B 看不到 A 的 implementation 依赖的符号(符号在 A 的编译 classpath 中但不在 B 的编译 classpath 中),但运行时这些类仍然存在(因为最终 APK 包含所有传递依赖)。这强制开发者只能通过 A 暴露的公开 API 使用 A,不能直接依赖 A 的内部实现。

Gradle 构建时的 classpath 分离:Gradle 为每个模块维护三个 classpath:

  1. compileClasspath:编译当前模块所需的依赖(包含 api 传递,不包含 implementation 传递)
  2. runtimeClasspath:运行当前模块所需的依赖(包含所有传递)
  3. annotationProcessorPath:APT 所需的依赖

implementation 依赖只出现在 runtimeClasspath 中,不出现在 compileClasspath 中。这就是”编译期隔离”的本质。

在大型组件化工程中,通常还配置 resourcePrefix 来隔离资源名冲突:

// module_user 的 build.gradle
android {
resourcePrefix "user_" // 该模块所有资源必须以 user_ 开头
}

这避免了不同模块定义了同名资源文件(如两个模块都有 bg_button.xml)导致的资源覆盖问题。

2.1 AndroidManifest 合并策略

多模块环境下,每个 library 模块有自己的 AndroidManifest.xml,构建时需要合并所有 Manifest。合并规则遵循优先级:

build variant Manifest > product flavor Manifest > main Manifest > library Manifest

冲突解决通过 tools:replacetools:merge 属性:

<!-- 在 app 模块的 Manifest 中解决冲突 -->
<activity
android:name=".MainActivity"
android:theme="@style/AppTheme"
tools:replace="android:theme" /> <!-- 用自己的 theme 替换 library 的 -->

<activity
android:name=".SplashActivity"
android:configChanges="orientation|screenSize"
tools:node="merge" /> <!-- 合并 library 和自己声明的属性 -->

Manifest Merger 的冲突日志级别可以通过 Gradle 配置调整:

android {
applicationVariants.all { variant ->
variant.outputs.all { output ->
output.processManifestProvider.configure {
doLast {
// 打印详细的 Manifest 合并报告
}
}
}
}
}

三、组件管理器:ComponentManager 设计

一个标准的组件管理器负责组件的注册、初始化和生命周期管理:

public class ComponentManager {
private static ComponentManager instance;
private final Map<String, IComponent> componentMap = new LinkedHashMap<>();

public static ComponentManager getInstance() { /* 单例 */ }

// 注册组件
public void registerComponent(String name, IComponent component) {
componentMap.put(name, component);
}

// 初始化所有组件(在 Application.onCreate 中调用)
public void initComponents(Application app) {
for (IComponent comp : componentMap.values()) {
comp.onCreate(app);
}
}

// 按需获取组件暴露的服务
public <T extends IService> T getService(Class<T> serviceClass) {
for (IComponent comp : componentMap.values()) {
if (serviceClass.isAssignableFrom(comp.getClass())) {
return (T) comp;
}
}
return null;
}

// 分发给所有组件的低内存回调
public void onLowMemory() {
for (IComponent comp : componentMap.values()) {
comp.onLowMemory();
}
}
}

// 组件接口
public interface IComponent {
void onCreate(Application app);
void onTerminate();
void onLowMemory();
void onTrimMemory(int level);
}

// 业务服务接口
public interface IUserService extends IService {
boolean isLoggedIn();
UserInfo getCurrentUser();
}

在编译期,每个模块通过 APT 自动生成注册代码:

// 自动生成的 UserComponentRegister.java(通过 APT 生成)
public class UserComponentRegister implements IComponentRegister {
@Override
public void register(ComponentManager manager) {
manager.registerComponent("user", new UserComponent());
}
}

3.1 组件初始化优先级

在有依赖关系的组件之间(如日志组件应在业务组件之前初始化),需要初始化优先级:

public @interface ComponentPriority {
int value() default 0; // 数值越小越先初始化
}

// APT 生成时记录优先级
// ComponentManager 中排序
componentMap.values().stream()
.sorted(Comparator.comparingInt(c -> c.getClass().getAnnotation(ComponentPriority.class).value()))
.forEach(comp -> comp.onCreate(app));

四、路由表生成:APT 与 ActivityRouter 方案

路由是组件化最核心的基础设施。模块 A 需要跳转到模块 B 的页面,但不能直接 import 模块 B 的 Activity 类(这会破坏组件隔离)。

ARouter(阿里开源,https://github.com/alibaba/ARouter)是使用最广泛的方案:

// 使用 @Route 注解标记目标页面
@Route(path = "/user/LoginActivity")
public class LoginActivity extends AppCompatActivity {
@Autowired
String username; // 自动从 URL 参数中注入

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ARouter.getInstance().inject(this); // 注入参数
}
}

// 跳转代码(不 import LoginActivity)
ARouter.getInstance().build("/user/LoginActivity")
.withString("username", "alice")
.navigation();

4.1 APT 如何生成路由表

ARouter 的 arouter-compiler 模块在编译期扫描所有 @Route 注解:

// arouter-compiler 的核心处理逻辑(简化)
// com.alibaba.android.arouter.compiler.processor.RouteProcessor
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
for (Element element : routeElements) {
Route route = element.getAnnotation(Route.class);
String path = route.path();
// 生成形如: atlas.put("/user/LoginActivity", RouteMeta.build(...))
// RouteMeta 包含 Class 引用、路由类型等
}
// 写入生成的文件:ARouter$$Group$$user.java
}

最终生成的代码:

// ARouter$$Group$$user.java(自动生成)
public class ARouter$$Group$$user implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/user/LoginActivity",
RouteMeta.build(RouteType.ACTIVITY, LoginActivity.class,
"/user/LoginActivity", "user", 0, 0));
}
}

ARouter 运行时通过 path 的第一段(如 “user”)作为 group,懒加载对应的 Group 文件,然后从 atlas 中找到目标 Class,完成跳转。

4.2 ARouter 的分组懒加载机制

ARouter 不一次性加载所有路由表,而是按 group 懒加载。path 的格式是 /group/pageName,第一段即 group。Warehouse.groupsIndex 维护 group → IRouteGroup class 的映射:

// ARouter 懒加载核心
protected Postcard build(String path) {
// 1. 从 Warehouse.routes 中查找
RouteMeta routeMeta = Warehouse.routes.get(path);
if (routeMeta == null) {
// 2. 提取 group,懒加载路由表
String group = path.substring(1, path.indexOf("/", 1));
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(group);
if (groupMeta == null) {
throw new NoRouteFoundException("no route match the path: " + path);
}
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
// 加载完成后从 groupsIndex 中移除(避免重复加载)
Warehouse.groupsIndex.remove(group);
routeMeta = Warehouse.routes.get(path);
}
// 3. 构建 Postcard
return new Postcard(path, routeMeta);
}

4.3 拦截器链

ARouter 支持全局拦截器,可在页面跳转前做登录检查等逻辑:

@Interceptor(priority = 1, name = "登录拦截器")
public class LoginInterceptor implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
if (postcard.getExtra() == NEED_LOGIN && !UserManager.isLogin()) {
callback.onInterrupt(new RuntimeException("请先登录"));
// 可在此处拦截并跳转到登录页
} else {
callback.onContinue(postcard);
}
}
}

拦截器链实现的底层是责任链模式。所有 @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)定义接口
public interface ILoginService extends IProvider {
boolean isLogin();
String getUserToken();
void logout();
}

// 在具体业务模块实现
@Route(path = "/user/LoginService")
public class LoginServiceImpl implements ILoginService {
@Override
public boolean isLogin() {
return UserManager.getInstance().isLogin();
}
@Override
public String getUserToken() { /* ... */ }
@Override
public void init(Context context) { /* ... */ }
@Override
public void logout() { /* ... */ }
}

// 在任何模块中调用(无需 import LoginServiceImpl)
ILoginService loginService = ARouter.getInstance().navigation(ILoginService.class);
if (loginService != null && loginService.isLogin()) {
// ...
}

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(主模块)
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 通过 APT 获取所有 IModule 实现并调用 init
List<IModule> modules = getAllModulesByApt();
for (IModule module : modules) {
module.init(this);
module.registerLifecycle(lifecycleCallbacks);
}
}
}

// 每个模块实现 IModule
public class UserModuleInit implements IModule {
@Override
public void init(Application app) {
// 初始化用户数据库、第三方登录 SDK 等
}
}

各模块的 IModule 实现类通过 APT 自动收集,主模块无需逐一引用。

6.1 ContentProvider 自动初始化技巧

Android 的一个设计特性被广泛利用于组件初始化:ContentProvider 的 onCreate() 在 Application 的 onCreate() 之前被调用。Firebase、WorkManager、App Startup 等库都利用这个特性:

// 在每个模块中声明一个 Provider
public class UserModuleInitProvider extends ContentProvider {
@Override
public boolean onCreate() {
// 此时 Application 的 onCreate 还没调用
// 但 Context 已经可用(getContext() 返回 Application)
UserModule.init(getContext());
return false; // 返回 false 表示不需要后续 CRUD 操作
}
// ... 其他方法返回 null 即可
}

每个模块在 AndroidManifest 中声明这个 Provider,系统自动在 App 启动时调用所有 Provider 的 onCreate。这实现了模块初始化的完全自动发现——主模块无需知悉任何子模块的初始化代码。

七、工程结构最佳实践

├── app/                    # 主模块(壳工程,只做拼装和 Application 初始化)
├── common/ # 公共层(BaseActivity、工具类、网络框架封装)
│ ├── common-base/
│ └── common-service/ # 各模块暴露的 IService 接口定义
├── module_user/ # 用户模块(可独立运行 runAlone = true)
├── module_im/ # 即时通讯模块
├── module_pay/ # 支付模块
└── buildSrc/ # Gradle 构建脚本的统一配置

runAlone 模式:每个业务模块在开发时可以单独作为 App 运行,通过 Gradle 配置:

// module_user 的 build.gradle
boolean runAlone = isRunAlone.toBoolean()

if (runAlone) {
apply plugin: 'com.android.application'
applicationId "com.example.module.user"
} else {
apply plugin: 'com.android.library'
}

runAlone = true 时,该模块就是一个独立的 Application,有自己的 AndroidManifest.xml,开发者可以独立启动和调试,无需编译其他模块。这在团队规模较大时极大地提升了开发效率。

7.1 多模块版本管理

大型组件化项目中,统一管理各模块的外部依赖版本至关重要:

// buildSrc/src/main/kotlin/Versions.kt
object Versions {
const val compileSdk = 33
const val minSdk = 21

object Libs {
const val okhttp = "4.9.3"
const val retrofit = "2.9.0"
const val glide = "4.14.2"
const val arouter = "1.5.2"
const val room = "2.5.0"
}
}

// 各模块 build.gradle.kts 中引用
dependencies {
implementation("com.squareup.okhttp3:okhttp:${Versions.Libs.okhttp}")
}

更进一步,可以使用 Gradle 的 platformenforcedPlatform 统一版本约束,或在 buildSrc 中使用 DependencyHandler 扩展函数统一依赖声明。

7.2 增量编译加速策略

大型组件化项目的编译速度优化:

  1. Gradle Build Cache:启用远程或本地缓存 org.gradle.caching=true
  2. 配置文件(Configuration Cache):Gradle 7.0+ 启用 org.gradle.configuration-cache=true
  3. 按需编译:只编译变更的模块及其下游依赖,使用 Gradle Enterprise 的 build scan 分析构建瓶颈
  4. **Kotlin Symbol Processing (KSP)**:替代 KAPT,编译速度提升 2-3 倍
  5. 避免传递依赖膨胀:使用 implementation 代替 api 减少编译 classpath 大小
  6. 并行构建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
打赏
  • 微信
  • 支付宝

评论