Appearance
Android 组件化开发
请说明 Android 组件化开发的核心思想,以及如何解决组件间通信、资源冲突、路由跳转这三个核心问题?
一、组件化开发的核心思想
组件化是 Android 大型项目的主流架构模式,核心思想是 “拆分、解耦、复用、集成”:
将一个庞大的 App 按 业务功能/职责 拆分为多个独立的“组件”(如首页组件、用户组件、订单组件、公共组件),每个组件可独立编译、调试、测试(作为单独 App 运行),最终通过“集成模式”组合为完整 App。
核心目标:
解耦:组件间低依赖/无依赖,避免“牵一发而动全身”;
并行开发:不同团队负责不同组件,提升开发效率;
复用:公共功能(如登录、支付、工具类)封装为组件,多 App 复用;
维护性:单个组件体积小,便于定位问题和迭代。
关键原则:组件边界清晰、依赖关系单向(上层组件依赖下层组件,禁止循环依赖)。
二、组件化核心问题的解决方案(实战落地)
1. 组件间通信:解决“无依赖组件如何交换数据/调用功能”
组件间禁止直接依赖(如订单组件不能直接引用用户组件的类),需通过“中间层”实现通信,主流方案有 2 种:
(1)路由框架 + 接口暴露(主流:ARouter)
核心思路:通过路由框架(如 ARouter)作为通信桥梁,组件通过“暴露接口”提供服务,其他组件通过接口调用,无需关心实现。
步骤实现:
新建基础组件(base-component):存放公共接口(IProvider),所有业务组件依赖该基础组件;
提供服务的组件(如用户组件):实现基础组件中的接口,用
@Route注解暴露服务;调用服务的组件(如订单组件):通过 ARouter 获取接口实例,调用方法(无直接依赖)。
代码示例:
基础组件定义接口:
Kotlin// 基础组件中的公共接口 interface UserService : IProvider { fun getCurrentUserId(): String // 获取当前用户ID fun getUserInfo(userId: String): UserInfo // 获取用户信息 }用户组件实现接口:
Kotlin// 用户组件中实现接口(隐藏实现细节) @Route(path = "/user/service") // 暴露服务的路由地址 class UserServiceImpl : UserService { override fun getCurrentUserId(): String { return "user_123" // 实际场景从本地缓存/数据库获取 } override fun getUserInfo(userId: String): UserInfo { return UserInfo(userId, "张三", "13800138000") } override fun init(context: Context?) {} }订单组件调用服务:
Kotlin// 订单组件中通过ARouter获取服务,无依赖用户组件 val userService = ARouter.getInstance().build("/user/service").navigation() as UserService val userId = userService.getCurrentUserId() val userInfo = userService.getUserInfo(userId) Log.d("OrderComponent", "当前用户:${userInfo.userName}")
(2)EventBus/Flow 事件通信(适用于“通知类场景”)
对于“无需返回结果”的通信(如用户登录成功后通知其他组件刷新),用 EventBus 或 Kotlin Flow 发送事件,组件订阅事件即可。
示例(Flow):
基础组件定义事件:
Kotlin// 基础组件中的事件模型 data class UserLoginEvent(val userId: String)用户组件发送事件(登录成功后):
Kotlin// 用户组件:登录成功后发送事件 fun loginSuccess(userId: String) { EventBus.post(UserLoginEvent(userId)) // 或用Flow的SharedFlow发送 }订单组件订阅事件:
Kotlin// 订单组件:订阅登录事件,刷新订单列表 @Subscribe(threadMode = ThreadMode.MAIN) // EventBus注解 fun onUserLogin(event: UserLoginEvent) { refreshOrderList(event.userId) // 刷新当前用户的订单 } // 注册/取消订阅(避免内存泄漏) override fun onStart() { super.onStart() EventBus.getDefault().register(this) } override fun onStop() { super.onStop() EventBus.getDefault().unregister(this) }
通信场景总结:
需返回结果、同步调用:用 ARouter + 接口暴露;
无返回结果、异步通知:用 EventBus/Flow。
2. 资源冲突:解决“多组件资源名重复”
多组件中若存在同名资源(如 ic_back.png、string_app_name),编译时会冲突,解决方案:
(1)资源命名规范 + 前缀约束
强制约定:所有组件的资源名必须加“组件前缀”(如首页组件前缀
home_、订单组件前缀order_);示例:首页组件的返回图标
ic_home_back.png,订单组件的返回图标ic_order_back.png,避免重名。
(2)Gradle 配置 resourcePrefix(强制前缀)
在组件的 build.gradle 中配置资源前缀,编译时自动校验,未加前缀的资源会报错:
Plain
android {
defaultConfig {
// 强制当前组件所有资源名以“order_”为前缀
resourcePrefix "order_"
}
}(3)公共资源统一存放
将全局复用的资源(如主题、颜色、尺寸、基础图标)统一放在「基础组件(base-component)」的 res 目录下,业务组件直接引用,避免重复定义。
3. 路由跳转:解决“无依赖组件间页面跳转”
组件间不能直接用 Intent(context, OtherComponentActivity::class.java) 跳转(会产生直接依赖),需通过路由框架(如 ARouter)实现“隐式跳转”。
(1)ARouter 路由跳转核心用法
组件内页面添加路由注解:
Kotlin// 订单组件的订单详情页(添加路由注解) @Route(path = "/order/detail") // 路由地址:/组件名/页面名 class OrderDetailActivity : AppCompatActivity() { // 接收参数(ARouter自动注入) @Autowired lateinit var orderId: String // 订单ID(必填参数) @Autowired(name = "isFromHome") var isFromHome: Boolean = false // 可选参数(默认值false) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ARouter.getInstance().inject(this) // 注入参数 Log.d("OrderDetail", "订单ID:$orderId") } }其他组件跳转该页面:
Kotlin// 首页组件跳转订单详情页(无直接依赖) ARouter.getInstance() .build("/order/detail") // 目标路由地址 .withString("orderId", "order_456") // 传递字符串参数 .withBoolean("isFromHome", true) // 传递布尔参数 .withTransition(R.anim.slide_in_right, R.anim.slide_out_left) // 跳转动画 .navigation() // 执行跳转
(2)路由跳转进阶能力
拦截器:实现登录拦截(如未登录时跳转到登录页,登录后再跳转目标页面);
降级策略:跳转不存在的路由时,显示错误页面或执行默认操作;
结果返回:支持
startActivityForResult类似的回调,获取跳转页面的返回数据。
示例:登录拦截器
Kotlin
// 全局登录拦截器(实现ARouter的IInterceptor)
@Interceptor(priority = 1) // 优先级:数值越小优先级越高
class LoginInterceptor : IInterceptor {
override fun process(postcard: Postcard, callback: InterceptorCallback) {
val path = postcard.path
// 需登录才能访问的页面(如订单详情、个人中心)
val needLoginPaths = listOf("/order/detail", "/user/profile")
if (needLoginPaths.contains(path) && !UserManager.isLogin()) {
// 未登录:跳转到登录页,携带目标页面路由地址(登录成功后跳转)
postcard.withString("targetPath", path)
.withSerializable("targetParams", postcard.extras)
.setDestination("/user/login") // 登录页路由地址
}
callback.onContinue(postcard) // 继续跳转
}
override fun init(context: Context?) {}
}三、组件化开发关键补充
组件模式切换:通过 Gradle 构建变量控制组件是“独立运行模式”还是“集成模式”(如
isModule = true时作为独立 App 运行);基础库设计:基础组件需包含:路由接口、公共资源、工具类、网络框架封装、数据库封装等,避免业务组件重复造轮子;
集成测试:通过“壳工程(app-module)”依赖所有业务组件,实现整体集成测试,壳工程仅负责组装组件,不包含业务逻辑。
总结
组件化核心:拆分解耦、单向依赖、独立开发、集成部署;
三大核心问题解决方案:
通信:ARouter + 接口暴露(需返回结果)、EventBus/Flow(通知类);
资源冲突:前缀命名规范 + Gradle resourcePrefix 强制约束;
路由跳转:ARouter 实现无依赖跳转,支持参数传递、拦截器、动画;
关键工具:ARouter(路由/通信)、EventBus/Flow(事件通知)、Gradle 构建配置(模式切换/资源约束)。
接下来是第八道面试题,请说明 Android 内存泄漏的常见原因及对应的排查方法、解决方案?并举例说明 3 种典型的内存泄漏场景(如单例持有 Context)?