Android组件化实战:分层架构、路由协作与Gradle改造全解析
1. 组件化到底在解决什么问题先聊清楚为什么要拆在我参与过的几个中等规模 App 项目里都有一个非常相似的成长轨迹业务迭代越来越快代码量越堆越大git 仓库越来越难管理编译时间越来越离谱。真正让我下决心做组件化的导火索是有一次只改了一个工具类的返回值类型结果全工程重新编译整整等了七分多钟。那会儿旁边同事开玩笑说这时间都够把今天的发布说明写完三遍了。组件化的核心思路其实不复杂把一个庞大的单体工程拆成若干个可以独立编译、独立测试的工程模块。模块之间不直接互相引用而是通过路由、服务接口这些约定好的方式协作。听起来简单但拆完之后整个研发节奏都不一样了——改某个业务模块只需要等那个模块编译完多人并行开发也不再天天因为合并冲突互相扯皮一些通用能力还能沉淀成公司内部的标准化组件被多个业务复用。不过我得说句实话组件化不是万能药更不是拿来炫技的架构表演。我在社区里见过不少团队项目总共两三个人业务还没跑通先花两周时间搭了一套完备的组件化框架最后被路由维护、服务注册这些额外复杂度拖得动弹不得。所以这篇文章我先不急着给方案而是把组件化真正解决的问题以及适用条件讲透再展开我在改造过程中用到的方法和踩过的坑。1.1 单工程模式下最折磨人的三个痛点第一个痛点是编译时间。代码量超过一定体量之后开发期随便改一行代码都需要等待全量编译。全量构建一台普通的开发机三分钟起步五分钟正常一些超大工程甚至要十几分钟。这种等待不只是浪费时间更致命的是它会打断心流状态——改一行、等五分钟、忘了刚才想改什么这个循环太伤效率了。第二个痛点是模块间的耦合。业务 A 图省事直接 new 了业务 B 的内部类业务 C 需要数据直接去拿业务 D 的静态方法。平时这么干确实快但等到业务 B 的构造函数变了、业务 D 的字段改名了连锁的编译错误会像多米诺骨牌一样倒一大片。最难受的是没人说得清谁依赖谁谁改了会影响到谁。第三个痛点是多人协作冲突。整个团队在一个代码仓库里开发多人同时编辑重叠的目录区域一天能遇到好几回 merge 冲突。解决冲突本身不麻烦麻烦的是解决完冲突之后要重新验证自己那部分逻辑没被改坏。人一多等待、沟通、验证的时间加到一起远超实际编码时间。1.2 组件化、模块化、插件化三个概念别混在一起这个领域有个老生常谈但很容易混淆的问题。我的理解是这样模块化说的是代码组织层面的事把逻辑按包名、按文件切分清楚本质上还是一个工程、一个编译单元组件化是工程构建层面的拆分每个组件都是独立的 Gradle Module可以单独编译成 Apk 或 AAR插件化更进一步涉及到运行时的动态加载跟组件化解决的问题完全不在一个维度上。我习惯用收纳来类比模块化是给衣柜分格衣服还是放在同一个柜子里只是分类摆好了组件化是把大开间改造成独立房间每个房间有自己的门锁和独立水电插件化则是不用改动房屋结构直接从窗户递一个新家具进来就能用。难度和代价完全不是一个级别。很多人嘴上说在做组件化实际上只做了模块化这不丢人清楚自己在哪个阶段很重要。1.3 什么情况下才值得动手做组件化我的判断标准很朴素单工程全量编译时间超过三分钟、业务线超过三条、日常协作人数超过十人这三个条件至少满足两个再考虑组件化。如果项目还小老老实实把代码组织好、把包结构理清楚比硬上组件化更有效。另外还要做好心理准备组件数量越多拆分带来的沟通成本和基础设施成本也会上升。组件多了之后路由表要维护、服务接口要配套、版本要统一这些都要有人持续负责。组件化更适合已经走到瓶颈期的团队而不是为了预演大厂架构、为了简历上多一行描述就去搞的。2. 组件边界怎么划分层架构与依赖规则的落地组件化改造最关键的一步不是写代码而是划分边界。我在第一次改造时犯过一个典型错误想一步到位把每个业务页面都做成一个组件结果组件数量爆炸依赖关系一团乱麻最后不得不推倒重来。第二次改造我学乖了先用一套清晰的分层架构把先后顺序和依赖方向定死再往下动手。这套方案里我采用的是业界比较通用的四层结构。顶层是 App 壳工程负责程序入口、整体的初始化流程和所有组件的装配下面一层是业务组件层像订单、用户、消息这类代表真实业务线的模块再往下是功能组件层比如网络库封装、图片加载封装、日志上报这类被多个业务复用的基础设施最底层是基础库层放工具类、通用 UI 组件和第三方 SDK 的统一封装。这里说一句可能得罪人的经验功能组件层是最容易被忽略的一层。很多团队拆完业务组件就不管了公共代码不知道放哪儿最后堆在 App 壳里或者到处拷贝。功能组件层一旦缺失上层业务组件之间会不可避免地互相借代码依赖关系早晚会失控。2.1 四层结构里每一层该放什么基础库层是最底层不依赖任何业务组件只依赖第三方 SDK 和最普通的工具类。功能组件层构建在基础库之上提供给业务层可复用的能力但它不应该知道任何具体业务的存在。业务组件层是按业务线切的比如支付组件、会员组件、社区组件它们可以依赖功能组件层但彼此之间尽量不要直接依赖。App 壳只有一个它负责把各个业务组件组合起来是整个工程的装配入口。判断一个模块到底该放哪一层我有一个简单的方法问自己这个东西离开具体业务还能不能独立存在。能就往下放不能就留在业务层。比如图片加载封装它不需要知道业务是什么就是典型的底层能力比如用户成就徽章这种必须理解用户业务的模块只能待在业务组件层。2.2 依赖规则严禁反向依赖跨层也不行这条规则我踩过坑所以必须单独拿出来说。四层结构里依赖只能从上往下走上层可以依赖下层下层绝不能反过来依赖上层。业务组件之间也尽量不要直接依赖对方。如果业务 A 要展示业务 B 的某个页面或者获取业务 B 的数据应该通过路由跳转或者服务接口的方式去实现而不是在代码里直接引用业务 B 的类。为什么会这么苛刻因为依赖方向一旦乱掉组件独立编译的价值就没了。你拆组件就是想改业务 B 的时候不影响业务 A但如果业务 A 编译依赖业务 B那业务 B 一改动业务 A 还得跟着编译一起验证等于没拆。2.3 业务组件的粒度怎么定按业务线而不是按页面拆组件的大小很考验判断力。我的经验是业务组件按业务线切不按页面切更不按功能点切。比如下单流程可以是一个组件但下单确认页就不应该单独成组件。粒度太细会导致组件数量失控、通信成本激增粒度太粗又退化成原来的单体大工程拆分失去意义。划分粒度时我还会参考另外一个维度变更频率。把经常一起变动的代码尽量放在同一个组件内把各自独立演进的代码拆分出去。这一条原理来源于经验并非严密理论但实践下来真的能显著减少跨组件的改动。3. 路由、服务与调试开关组件协作的三个技术抓手边界划分清楚了接下来的问题就是组件之间怎么协作。在组件化方案里组件不直接互相依赖那页面跳转怎么办、数据获取怎么办、方法调用怎么办我分别用了三个技术手段来解决路由、服务接口、调试开关。这三个不是全部但属于最核心的框架支撑。一开始做组件化时我也有点担心不直接依赖代码还能正常跑起来吗实际做下来发现只要把组件之间交互的通道设计好不仅没问题代码结构还会比以前清晰得多。因为组件之间的交互全部都变得有迹可循了不再是那种冥冥之中靠运气编译通过的引用关系。3.1 组件路由页面跳转的显式解耦页面跳转是最常见的跨组件协作场景。例如在订单组件里点击按钮要跳到用户组件的某个页面。传统写法是直接 startActivity 传入目标 Activity 的类组件化之后不能这么干了因为订单组件根本没有依赖用户组件。我采用的方案是经典的路由表机制每个组件在 Application 初始化时把自己的页面路径注册到全局路由表中。跳转方不关心目标页面在哪个组件里实现只传一个字符串路径和参数由路由框架负责解析和完成跳转。// 路由注册通常在组件自己的初始化类里执行 Router.register(route://user/profile, ProfileActivity::class.java) // 路由跳转可在任何组件内执行无需依赖目标组件 Router.navigate(context, route://user/profile, bundleOf(userId to 1024))这个方案的代价是丢掉了编译期检查路径写错了、参数传错了都要到运行期才能发现。我的补救办法是建立路由文档把每个组件对外提供的所有路径和参数统一登记定期人工核对。虽然没有编译期那么省心但至少能拦截掉大部分低级错误。3.2 服务接口跨组件调方法的正确姿势页面跳转解决了界面层的协作但数据获取、方法调用这种需求光靠路由就不够了。我的做法是定义一个接口层把某个业务对外提供的能力抽象成服务接口然后让具体的业务组件去实现这个接口。调用方只需要依赖接口定义不依赖任何具体实现类。以用户组件为例。用户组件对外提供一个获取用户昵称的接口实现部分在用户组件自己内部完成。// 接口定义本身放在一个公共模块里比如功能组件层或基础库 interface UserService { fun getUserName(userId: String): String? } // 用户组件内部提供实现并注册到服务容器 class UserServiceImpl : UserService { override fun getUserName(userId: String): String? { return queryUserFromDb(userId)?.name } } // 启动时注入 ServiceContainer.register(UserService::class.java, UserServiceImpl()) // 其他组件通过服务容器获取实现 val name ServiceContainer.get(UserService::class.java)?.getUserName(1024)这种设计下接口归属低层、实现在高层天然满足了依赖规则也不会破坏组件的独立编译。需要注意一点接口的入参和出参尽量用基本类型或可序列化的数据模型别直接把一个组件里的自定义对象传到另一个组件里去跨组件传自定义对象会引入隐式依赖。3.3 独立调试开关一套代码在 library 和 application 之间切换组件要能独立调试不然开发时每次都得起整个 App 工程效率优势就缩水了。我的做法是通过 gradle.properties 里的一个布尔开关控制打开时业务组件作为 application 运行拥有自己的 Application、自己的启动页、自己的一套测试数据关闭时它作为 library 被 App 壳依赖。具体配置留到第 4 节详细说这里想先讲清楚设计思路。每个业务组件里要同时维护两套代码一套是独立运行需要的调试入口和假数据一套是真正作为 library 集合进 App 里的逻辑。这两套代码的边界要划分清晰通常放在同一个 debug 或者 standalone 的代码目录下避免污染正式逻辑。3.4 资源冲突与基础能力公共资源命名前缀不能省还有一个容易翻车的点是资源文件。所有组件都是平行独立编译的如果两个组件里都有一个 ic_launcher 或者 common_colors.xml资源合并时就会冲突。我踩过一次这个坑之后给所有组件定了一条硬性规范每一个组件的资源文件必须使用自己的前缀比如订单组件用 order_、用户组件用 user_再配合 lint 检查强制约束。公共的样式、颜色、Dimens 则统一放到基础库层组件中业务组件里原则上不允许自定义全局主题相关的资源。这么做的好处除了避免冲突还能让 UI 风格保持统一设计师也省心。4. 从单工程到组件化Gradle 配置改造的完整过程写完架构和技术方案的文档后真正的体力活才开始把原来的单工程改造成多模块结构。这一节我按实际操作顺序把改造步骤列全每一步都说明为什么要这么做。Gradle 的版本迭代很快具体 DSL 写法可能会有差异但背后的逻辑这些年基本没变过。改造的目标是让每个业务组件都能独立调试同时在集成状态下可以被 App 壳组装运行。这个目标确定了所有配置改动都围绕它展开。4.1 第一步重构工程目录与 settings.gradle首先需要把代码按组件拆分到独立目录然后在 settings.gradle 里注册这些子模块。以订单组件、用户组件为例// settings.gradle include :app include :library-base include :component-network include :component-user include :component-order这个阶段要克制住重构代码的欲望。先把目录结构和 Gradle 配置切出来让工程能跑能编译再去动具体代码逻辑。如果一边切目录一边大刀阔斧改业务代码出了问题极难排查。4.2 第二步用 gradle.properties 定义组件开关在工程根目录的 gradle.properties 里加上这样一个开关# 组件化开关true 时业务组件独立运行false 时作为 library 被 app 依赖 IS_MODULEtrue这个开关会在每个组件的 build.gradle 里被读取。为什么用全局配置文件而不是直接改 build.gradle因为团队协作时大家需要统一开关状态。谁打开了独立调试、谁关掉了通过这一个文件的变更就能看出来避免有的人本机环境正常但别人拉代码之后编译失败。4.3 第三步业务组件的 build.gradle 按开关切换角色这是改造的核心一环。业务组件在独立调试时是 application在集成状态下是 library// component-user/build.gradle if (IS_MODULE.toBoolean()) { plugins { id com.android.application } } else { plugins { id com.android.library } } android { // 独立调试时使用独立的应用ID和特定版本 defaultConfig { if (IS_MODULE.toBoolean()) { applicationId com.example.user versionCode 1 versionName 1.0 } minSdk 21 targetSdk 34 } sourceSets { if (IS_MODULE.toBoolean()) { // 独立调试时使用独立的 AndroidManifest main.manifest.srcFile src/main/module/AndroidManifest.xml } else { main.manifest.srcFile src/main/AndroidManifest.xml } } }这段配置里最容易出错的是 AndroidManifest。作为 application 运行时Manifest 必须要有 application 标签和 launchable activity作为 library 时这些内容又不能保留或者不能起作用所以要把两套 Manifest 分开维护。我有一次就是这里漏了独立运行一切正常集成模式下却出现 application 属性冲突排查了整整一下午。4.4 第四步配置 App 壳的依赖方式App 壳在集成模式下要依赖所有业务组件在独立调试模式下则可以不依赖它们或者在 debug 下也把所有组件当作 library 引入以便进行整体联调。我一般让 App 壳始终依赖所有业务组件的 library 形态这样团队协同联调时能保证环境一致。真正日常开发单个组件时直接运行那个组件的 application 形态即可无需启动 App 壳。App 壳里还负责统一初始化各个组件。我在基础库层定义一个组件初始化接口App 壳的 Application 启动时按顺序调用每个组件的初始化方法避免每个组件都往 Application 里塞初始化代码也避免了单例 Application 类膨胀成上帝类。4.5 第五步统一版本管理与依赖配置拆组件后最担心的就是依赖版本打架两个组件各自依赖同一个第三方库的不同版本构建时经常出现各种诡异问题。我采用的方式是维护一个统一的依赖版本定义文件所有组件都从这份定义里取版本号不允许任何组件私自写死第三方版本。Gradle 圈里的做法迭代过好几轮从 ext 全局变量到 buildSrc 再到现在的 version catalog本质思路都相同统一版本入口构建时避免依赖冲突。我用下来比较推荐 version catalog它把依赖声明集中管理IDE 支持也好补全和跳转都很顺手。4.6 改造顺序的三个建议如果你准备在现有项目里动手我的建议顺序是先抽取基础库层把工具类、网络层封装从业务代码里剥离出来接着抽取功能组件层沉淀通用的能力模块最后再抽业务组件。每抽一个模块都要保证整个工程能正常编译通过再继续下一个。这个顺序可以让改造过程全程处于可发布状态。我在团队里推行时定了一条规矩任何一步拆分都不允许让主干长期处于红色构建状态否则问题一旦积累就再也不敢动了。拆完之后的编译收益是直观且明确的。我所在的工程原先全量构建在四分钟左右改造完核心业务组件后只编译用户组件差不多五十秒日常开发节奏舒服了一大截。5. 改造遇见的真实故障一次依赖冲突的完整排查再好的方案落到实际工程里都会遇到问题组件化的坑尤其多。这一节我完整复盘一次遇到过的依赖冲突故障把整个排查链路写清楚比直接列一个常见问题清单参考价值更大。那时候组件化改造刚完成一批功能组件层里有两个组件都依赖了同一个网络库的不同版本。本来以为版本号差异不大问题不大谁知在集成构建时冒出一堆 Duplicate class 报错而且报错信息把同一个类的两个不同来源都列了出来。这个报错在拆组件之前从来没出现过因为我只有一个模块依赖树顿时清晰现在变成多模块之后重复类第一次暴露出来。初步判断是某个依赖被传递引入了两次于是先执行了一次依赖树查询命令./gradlew :app:dependencies --configuration debugRuntimeClasspath这条命令会打印 App 壳在运行期依赖的完整依赖树哪个组件依赖了哪个库的哪个版本一目了然。排查下来发现两个功能组件都把网络库打进了自己组件的 class 里其中一个还错误地把网络库的 jar 文件通过 fileTree 方式直接引入了。这就是冲突的根源——如果每个人都拷贝一份库文件而不是通过 Gradle 坐标声明依赖各组件间的 class 必然会重复。找到根因后的修复方式分两步。第一步把 fileTree 引入的方式移除统一改成 Gradle 坐标依赖第二步在其中一个组件的 build.gradle 里用 exclude 排除掉多余的传递依赖dependencies { implementation(com.example:network:1.2.0) { exclude group: com.example, module: legacy-http } }重新构建后 Duplicate class 报错消失但几小时后集成编译又出现了新提示这次是资源文件重复。和前面思路一样我先用 lint 扫描了重复资源的来源发现两个组件各自拷贝了一份统一的 loading 动画资源。经过这次完整排查我算是彻底理解了组件化工程为什么必须坚持统一的依赖入口和资源前缀规范老话说的规范是低成本的防错机制在组件化改造里体现得尤其充分。这个故障也教会我一个排查思路组件化工程出问题时先别急着怀疑 Gradle 配置和代码逻辑第一时间跑构建的依赖报告或资源报告搞清楚冲突边界到底在哪里。90% 的组件化集成问题都能通过依赖树查出来剩下的再考虑代码层的关联。6. 组件化使用一段时间后的几句真心话改造完成后的半年里团队确实吃到了组件化的红利但我对它的认知也从一开始的万能解药慢慢变成了更务实的工具观。组件化本质上是一种控制变更影响力的工程手段它的价值不在于架构本身多么优雅而在于让大团队、大工程的日常开发回归到一个可以接受的心智负担范围内。如果你所在的团队也在考虑组件化我的建议是别等完美方案先拆一个最边缘的业务组件试一试跑通独立编译和集成编译的完整链路再逐步扩大范围。这个过程里最关键的评判标准永远只有一个工程是否始终能正常构建发布。只要主干随时是绿的改造就不会失控。最后再分享一个小技巧每次拆分提交时只做结构移动和配置调整不混入任何业务逻辑修改commit message 里写明重构拆分用户组件无功能变更。一旦后续出现神秘问题排查者可以放心地直接回滚这次提交而不用担心影响业务需求。组件化是一个持续演进的过程每一步走得稳比较重要。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →