29、构建系统实战案例:大型项目构建优化案例、模块化构建落地案例、CI/CD流水线搭建案例、构建问题排查案例
说实话,前面讲了那么多理论,今天终于到了我最喜欢的环节——实战。
你想想看,学了那么多 Gradle 配置、Task 编排、依赖管理,最终不就是为了解决实际项目中的问题吗?
这一章,我把自己这些年踩过的坑、填过的土,浓缩成四个典型案例。每个案例都是真实项目里摸爬滚打出来的,希望能帮你少走弯路。
一、大型项目构建优化案例
先说说大型项目。什么叫大型?我经历过一个项目,光 module 就有 80 多个,全量构建一次要 40 分钟。嗯,你没看错,40 分钟。每次改一行代码,等构建的时间够泡三杯咖啡了。
1.1 问题诊断:先找到瓶颈在哪
优化之前,得先知道慢在哪。我个人习惯用 --profile 和 --scan 两个参数。
// 生成构建报告
./gradlew assembleDebug --profile --scan
// 分析报告中的关键指标
// 1. 总耗时
// 2. 每个 Task 的执行时间
// 3. 配置阶段耗时
// 4. 依赖解析耗时
我记得第一次跑扫描,发现一个惊人的事实:光依赖解析就占了 12 分钟。为什么?因为项目里大量使用了动态版本号,比如 2.1.+ 这种写法。每次构建都要去远程仓库检查最新版本,能不慢吗?
1.2 优化策略:三板斧
第一板斧:配置缓存。说白了,就是让 Gradle 记住上次的配置结果,没变化就别重新算。
// gradle.properties
org.gradle.configuration-cache=true
org.gradle.configuration-cache.parallel=true
第二板斧:并行构建。这个默认是关的,得手动开。
// gradle.properties
org.gradle.parallel=true
org.gradle.workers.max=4 // 根据 CPU 核心数调整
第三板斧:按需配置。只配置当前需要的 module,别把 80 个 module 全加载了。
// settings.gradle.kts
include(":app")
include(":core:network")
// 不要 include 所有 module,只 include 需要的
二、模块化构建落地案例
模块化,说白了就是把一个大项目拆成多个小模块。但怎么拆?拆到什么粒度?这里头有讲究。
2.1 模块划分原则
我个人习惯按业务功能划分,而不是按层划分。举个例子:
| 错误做法 | 正确做法 |
|---|---|
|
|
为什么?因为按层划分,改一个登录功能,你得同时改 data、domain、presentation 三个模块。而按业务划分,改登录只需要改 :feature:login 一个模块。你想想看,哪个更清晰?
2.2 模块间通信
模块化最大的坑就是模块间耦合。我曾经在一个项目里,看到 :feature:login 直接依赖了 :feature:home 的某个类。这哪是模块化?这是把大泥球拆成了小泥球。
正确的做法是:模块间通过接口通信,不直接依赖实现。
// :core:router 模块中定义接口
interface LoginRouter {
fun navigateToHome(context: Context)
}
// :feature:login 中调用
class LoginViewModel(private val router: LoginRouter) {
fun onLoginSuccess() {
router.navigateToHome(context)
}
}
// :app 中实现依赖注入
class AppLoginRouter : LoginRouter {
override fun navigateToHome(context: Context) {
context.startActivity(Intent(context, HomeActivity::class.java))
}
}
api 而不是 implementation 暴露接口模块。这样下游模块能直接使用接口,不用重复依赖。
三、CI/CD 流水线搭建案例
CI/CD 流水线,说白了就是让机器帮你做重复劳动。我见过太多团队,每天手动打包、手动测试、手动发布。嗯,效率低不说,还容易出错。
3.1 流水线设计
一个标准的 Android CI/CD 流水线,我建议分这几个阶段:
// 以 GitHub Actions 为例
name: Android CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run lint
run: ./gradlew lint
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run unit tests
run: ./gradlew test
build:
needs: [lint, unit-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build APK
run: ./gradlew assembleRelease
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: app-release.apk
path: app/build/outputs/apk/release/
3.2 关键配置
搭建流水线时,有几个点特别容易踩坑:
- 缓存配置:每次 CI 都重新下载依赖?太慢了。记得缓存
~/.gradle/caches目录。 - 并行任务:lint 和 unit test 可以并行跑,别串行。
- 构建变体:CI 上只构建 release 变体,debug 变体留给本地开发。
local.properties 中显式指定了 SDK 路径,或者在 CI 环境变量中设置 ANDROID_HOME。
四、构建问题排查案例
最后说说排查问题。构建出问题了,别慌。我有一套排查流程,分享给你。
4.1 常见问题及解法
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 构建卡在某个 Task 不动 | Task 配置了 dependsOn 循环依赖 |
用 ./gradlew :app:dependencies 查看依赖树 |
| 依赖冲突报错 | 多个模块依赖了不同版本的库 | 用 ./gradlew :app:dependencyInsight --dependency xxx 定位 |
| 构建速度突然变慢 | 可能开启了 --no-daemon 或缓存被清空 |
检查 ~/.gradle/daemon 日志 |
| APK 体积异常大 | 引入了不必要的 native 库或资源 | 用 ./gradlew :app:lintVitalRelease 分析 |
4.2 我的排查三板斧
第一斧:看日志。别只看最后几行报错,要从头看起。很多时候真正的错误在前面。
第二斧:隔离排查。把问题模块单独拎出来构建,看能不能复现。如果能,那就是模块内部的问题;如果不能,那就是模块间依赖的问题。
第三斧:回滚对比。用 Git 回滚到上次能构建的版本,然后逐步前进,找到是哪次提交引入了问题。
知识体系总览
下面这张图,是我对本章四个案例的总结。你可以把它当作一个检查清单,遇到问题时对照着看。
好了,四个案例讲完了。说实话,每个案例背后都有我踩过的坑。但正是这些坑,让我对构建系统有了更深的理解。你如果遇到类似问题,不妨按我上面的思路试试。嗯,应该能帮你省不少时间。
公众号:蓝海资料掘金营,微信deep3321