16、性能优化:守护进程配置、并行构建、构建缓存、配置缓存、增量编译
说实话,Gradle 刚出来那会儿,很多人吐槽它慢。我自己也经历过——一个中型项目,每次构建都要等两分钟,喝口水回来还没跑完。后来我深入研究才发现,不是 Gradle 慢,而是我们没把它调教好。
这一章,我就把压箱底的性能优化技巧掏出来。五个核心方向:守护进程、并行构建、构建缓存、配置缓存、增量编译。每一个我都踩过坑,也找到了最佳实践。
16.1 守护进程:别让 JVM 反复重启
Gradle 守护进程,说白了就是一个常驻后台的 JVM 进程。你第一次构建时启动它,后续构建直接复用。我见过太多团队,每次构建都重新加载整个 Gradle 运行时,那感觉就像每次开车都重新打火一样——费油又费时。
核心配置(gradle.properties):
# 开启守护进程(默认就是 true)
org.gradle.daemon=true
# 设置守护进程的堆内存,我习惯给 2GB
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# 守护进程空闲超时(毫秒),默认 10800000(3小时)
org.gradle.daemon.idletimeout=3600000
我的经验:守护进程的堆内存别设太大,也别太小。2GB 对于大多数中型项目足够了。设太大反而会跟其他应用抢内存,导致系统卡顿。
守护进程的生命周期管理:
--daemon:显式启用守护进程--no-daemon:临时禁用(调试时有用)--stop:停止所有守护进程--status:查看守护进程状态
我曾经踩过的坑:有一次项目构建突然变慢,排查了半天才发现是守护进程的内存被耗尽了。原来有个同事在 CI 脚本里忘了设置 --no-daemon,导致多个构建任务共享同一个守护进程,互相干扰。后来我强制在 CI 里用 --no-daemon,本地开发才用守护进程,问题就解决了。
16.2 并行构建:让多核 CPU 忙起来
现在的笔记本至少 4 核 8 线程,但默认情况下 Gradle 是串行构建的。你想想看,这不是浪费资源吗?
并行构建的核心思想是:把没有依赖关系的任务放到不同线程里同时跑。比如编译 module A 和 module B,如果它们互不依赖,完全可以并行。
开启并行构建:
# gradle.properties
org.gradle.parallel=true
# 控制并行线程数(默认是 CPU 核心数)
org.gradle.workers.max=4
什么时候该用并行?
- 多模块项目(模块间依赖少)——效果最明显
- 单模块但任务多(比如同时跑 lint、test、compile)
- CI 服务器上——充分利用服务器资源
什么时候不该用?
- 单模块小项目——并行开销可能大于收益
- 模块间依赖链特别长——并行度有限
- 内存紧张——并行会消耗更多内存
我个人习惯:先在本地用 --parallel 跑一次,对比一下时间。如果提速超过 20%,就永久开启。如果只快了 5%,那就算了,不值得多占内存。
16.3 构建缓存:别重复造轮子
构建缓存,说白了就是把编译结果存起来。下次如果输入没变,直接拿缓存结果,跳过编译。这跟浏览器缓存是一个道理。
Gradle 有两种缓存:
- 本地缓存:存在
~/.gradle/caches/下 - 远程缓存:存在服务器上,团队共享
开启构建缓存:
# gradle.properties
org.gradle.caching=true
# 配置远程缓存(build.gradle)
buildCache {
local {
enabled = true
directory = "${System.getProperty('user.home')}/.gradle/caches/build-cache-1"
}
remote(HttpBuildCache) {
url = 'https://cache.example.com/cache/'
push = true
credentials {
username = 'cache-user'
password = 'cache-pass'
}
}
}
哪些任务可以被缓存?
compileJava、compileKotlin——编译任务processResources——资源处理jar、assemble——打包任务- 自定义任务——只要声明了
@CacheableTask
我曾经遇到的一个坑:有次远程缓存一直不生效,排查了半天才发现是缓存 key 的生成规则有问题。Gradle 默认把任务的输入(源码、配置、环境变量等)作为 key 的一部分。如果某个输入没被正确声明,缓存就会失效。比如自定义任务里用了 System.getProperty() 读取参数,但没有用 @Input 注解声明,那缓存就永远命中不了。
16.4 配置缓存:跳过配置阶段
Gradle 的构建过程分三个阶段:初始化 → 配置 → 执行。配置阶段会解析所有 build.gradle 文件,创建 Task 对象图。对于大型项目,这个阶段可能就要花 10-20 秒。
配置缓存(Configuration Cache)就是把这个阶段的结果序列化存起来。下次构建时,如果构建脚本没变,直接加载缓存,跳过配置阶段。
开启配置缓存:
# gradle.properties
org.gradle.configuration-cache=true
# 如果遇到问题,可以先启用严格模式排查
org.gradle.configuration-cache.problems=warn
配置缓存的要求:
- 构建脚本不能有副作用(比如写文件、网络请求)
- 不能使用
System.getProperty()等非序列化 API - Task 的输入输出必须明确声明
我的建议:配置缓存是 Gradle 7.0 以后才稳定的功能。如果你在用旧版本,先升级再说。另外,第一次开启时可能会报一堆警告,别慌,一个个修就行。我花了一个下午才把项目里的所有问题修完,但修完之后构建速度提升了 40%。
配置缓存 vs 构建缓存:
| 特性 | 配置缓存 | 构建缓存 |
|---|---|---|
| 缓存什么 | Task 配置图 | Task 执行结果 |
| 跳过什么 | 配置阶段 | 执行阶段 |
| 适用场景 | 构建脚本没变 | 源码没变 |
| 缓存失效 | 构建脚本变化 | 输入文件变化 |
16.5 增量编译:只改哪里编译哪里
增量编译,说白了就是只编译你修改过的文件。Java 编译器本身支持增量编译,但 Gradle 需要配合才能发挥效果。
Gradle 的增量编译机制:
- 跟踪每个源文件的哈希值
- 只重新编译哈希值变化的文件
- 如果修改了 public API,会级联编译依赖它的文件
开启增量编译:
# gradle.properties
# Java 增量编译(默认就是 true)
org.gradle.java.compile.incremental=true
# Kotlin 增量编译
kotlin.incremental=true
# 启用编译避免(跳过不必要的注解处理)
org.gradle.annotation.processing.avoidance=true
增量编译的最佳实践:
- 保持模块粒度适中——太大会导致级联编译范围大
- 避免循环依赖——会破坏增量编译
- 使用
--build-cache配合增量编译——效果翻倍 - 定期清理
build/目录——防止缓存膨胀
我曾经踩过的坑:有次增量编译突然失效了,每次改一个文件都要全量编译。查了半天,发现是某个第三方库的注解处理器没有声明 @SupportedOptions,导致 Gradle 无法判断它是否受增量编译影响。解决方案是升级那个库的版本,或者用 annotationProcessor 配置排除它。
16.6 综合优化策略
上面五个技巧单独用效果有限,组合起来才能发挥最大威力。我一般这样配置:
我的 gradle.properties 模板:
# 守护进程
org.gradle.daemon=true
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# 并行构建
org.gradle.parallel=true
org.gradle.workers.max=4
# 构建缓存
org.gradle.caching=true
# 配置缓存
org.gradle.configuration-cache=true
# 增量编译
org.gradle.java.compile.incremental=true
org.gradle.annotation.processing.avoidance=true
不同场景的优化重点:
| 场景 | 重点优化 | 预期提速 |
|---|---|---|
| 本地开发(频繁改代码) | 增量编译 + 守护进程 | 50%-80% |
| CI/CD(全量构建) | 构建缓存 + 并行构建 | 30%-60% |
| 大型多模块项目 | 配置缓存 + 并行构建 | 40%-70% |
| 首次构建(无缓存) | 守护进程 + 并行构建 | 20%-40% |
最后说一句:性能优化不是一劳永逸的事。项目在变,依赖在升级,Gradle 版本也在更新。我建议每季度做一次构建性能审计,用 --scan 生成构建报告,看看哪些环节还有优化空间。记住,优化的目标是让开发者少等几秒,而不是追求极致的数字。
公众号:蓝海资料掘金营,微信deep3321