48、模块化优化:模块划分原则、模块依赖管理、模块编译优化、模块测试
模块化,说白了就是把一个大系统拆成若干小块。每个小块独立开发、独立测试、独立编译。我做了这么多年Android系统优化,发现很多性能问题其实根源不在代码本身,而在模块划分不合理。
你想想看,一个App如果所有代码都塞在一个模块里,改一行代码就得全量编译,测试也得全量回归。这效率能高吗?更可怕的是,模块之间耦合太紧,牵一发而动全身,改一个功能可能引发十几个模块的连锁问题。
嗯,今天我们就来聊聊模块化优化的四个核心维度:怎么划分、怎么管依赖、怎么加速编译、怎么高效测试。
模块划分原则
模块划分不是拍脑袋决定的。我个人习惯遵循几个基本原则:
- 单一职责:一个模块只做一件事。比如网络请求模块就别掺和图片加载的逻辑。
- 高内聚低耦合:模块内部要紧密,模块之间要松散。说白了就是内部随便改,对外接口尽量稳定。
- 业务边界清晰:按业务领域划分,而不是按技术分层。比如“支付模块”、“用户模块”、“订单模块”,而不是“工具类模块”、“数据层模块”。
- 复用性优先:如果一个功能可能被多个模块使用,那就单独抽成一个公共模块。
核心原则:模块划分的最终目标,是让每个模块可以独立演进、独立测试、独立部署。做不到这三点,你的模块化就是伪模块化。
我在项目中遇到过一种情况:团队把所有的工具类都塞进了一个叫“common”的模块里。结果这个模块越来越大,最后变成了一个“垃圾堆”。改一个工具类,所有依赖它的模块都得重新编译。这就是典型的划分失败案例。
模块依赖管理
依赖管理是模块化的命门。依赖关系乱了,整个项目就成了一团乱麻。
我建议你遵循几个原则:
- 单向依赖:依赖方向要清晰,不能出现循环依赖。比如基础模块不能依赖业务模块。
- 依赖倒置:高层模块不依赖低层模块,两者都依赖抽象接口。这样换实现的时候,只需要改一个模块。
- 最小依赖原则:一个模块只依赖它真正需要的东西。别为了省事,把整个库都引进来。
举个例子,假设我们有三个模块:core(核心)、network(网络)、user(用户)。正确的依赖关系应该是:
// 正确的依赖关系
user --依赖--> network --依赖--> core
// 或者
user --依赖--> core
user --依赖--> network
千万别搞成:
// 错误的循环依赖
user --依赖--> network
network --依赖--> core
core --依赖--> user // 这就出问题了
避坑指南:我曾经在一个大项目里发现了一个隐藏的循环依赖。两个模块互相引用对方的接口,编译时没问题,但运行时偶尔会崩溃。查了三天才找到原因。从那以后,我强制团队在CI流程里加了依赖检查脚本。
依赖管理的工具方面,我推荐使用Gradle的api和implementation关键字。记住一个原则:
api:暴露给外部模块的依赖。比如你的模块对外提供了某个库的接口,那就用api。implementation:只在模块内部使用的依赖。外部模块不需要知道。这样可以减少编译时的依赖传递。
模块编译优化
编译速度是模块化最直接的收益点。但前提是你得配置得当。
我常用的几个优化手段:
- 增量编译:只编译修改过的模块。Gradle默认支持,但需要你正确配置模块间的依赖关系。
- 并行编译:多模块可以同时编译。在
gradle.properties里设置org.gradle.parallel=true。 - 缓存编译结果:使用Gradle的构建缓存,避免重复编译。特别是CI环境,缓存能省下大量时间。
- 按需编译:只编译你当前需要的模块。比如你在改
user模块,就别让Gradle去编译payment模块。
小技巧:在Android Studio里,你可以用Build > Select Build Variant来指定只编译某个模块的debug版本。配合--configure-on-demand参数,编译速度能提升30%以上。
我记得有一次,项目从单模块拆成20多个模块后,全量编译时间从15分钟降到了3分钟。但增量编译时间反而变长了。为什么?因为模块间依赖关系没理清,改一个模块导致十几个模块都得重新编译。后来我们重新梳理了依赖,把公共接口抽出来,增量编译时间才降下来。
模块测试
模块化之后,测试策略也得跟着变。你不能还像以前那样,每次改完代码就跑全量测试。
我建议采用分层测试策略:
- 单元测试:每个模块内部的功能测试。用JUnit + Mockito,测试粒度要细,覆盖核心逻辑。
- 模块内集成测试:测试模块内部多个类的协作。比如测试网络模块的请求-响应流程。
- 跨模块集成测试:测试模块间的接口是否正常工作。比如用户模块调用网络模块的场景。
- 端到端测试:模拟真实用户操作,测试整个App的流程。这个可以放在CI的夜间构建里跑。
测试的配置也很重要。我习惯在每个模块的build.gradle里单独配置测试依赖:
// 模块的 build.gradle
dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:4.0.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
这样做的好处是,每个模块的测试环境是独立的。你可以在模块级别运行测试,不用等整个项目编译完。
关键点:模块测试的核心是“隔离”。每个模块的测试只依赖它自己的代码和mock的外部依赖。不要因为测试一个模块,就得启动整个App。
我曾经踩过一个坑:某个模块的单元测试依赖了另一个模块的数据库操作。结果每次跑测试都得先初始化数据库,测试速度慢得离谱。后来我们把数据库操作mock掉,测试时间从5分钟降到了10秒。
模块化知识体系
下面这张图总结了模块化优化的核心逻辑,你可以对照着梳理自己的项目:
模块化优化不是一蹴而就的事。它需要你在项目初期就做好规划,在开发过程中持续维护。但一旦做对了,收益是巨大的——编译更快、测试更稳、团队协作更顺畅。
嗯,今天就聊到这里。记住一句话:模块化不是为了炫技,而是为了让你的代码更健康、团队更高效。