22、性能优化:构建扫描、构建分析、配置优化、任务优化
说到Gradle性能优化,我脑子里第一个蹦出来的词就是「构建扫描」。这东西太实用了。我记得刚接手一个大型微服务项目时,构建一次要7分钟,团队都快疯了。后来就是靠构建扫描这把手术刀,一刀刀切开了性能瓶颈。
说白了,性能优化不是玄学。你得先知道慢在哪,才能谈怎么优化。Gradle官方给了我们一套很完整的工具链,今天我就带你走一遍。
构建扫描(Build Scan)—— 性能问题的CT机
构建扫描是什么?它就是一个Web报告,记录了每次构建的完整细节。从任务耗时、依赖解析、配置阶段,到缓存命中情况,全都有。
启用方式:在构建命令后加 --scan 参数即可。
./gradlew build --scan
执行后,终端会输出一个链接。打开它,你会看到一个类似这样的报告页面。我个人习惯每次做性能调优前,先跑一次带扫描的构建,作为「基准线」。
构建扫描能告诉你三件核心事:
- 配置阶段耗时:Gradle在真正执行任务前,会先解析build.gradle文件。这一步如果超过5秒,就得警惕了。
- 任务执行耗时:每个任务跑了多久,有没有被缓存跳过。
- 依赖解析耗时:下载依赖花了多少时间,有没有重复解析。
小技巧:在CI环境中,我建议默认开启构建扫描。配合 --no-build-cache 参数,可以对比缓存开启前后的性能差异。
构建分析 —— 从数据中找规律
光看一次扫描还不够。你需要看趋势。Gradle提供了 build-scan 插件,可以长期收集数据。
// settings.gradle.kts
plugins {
id("com.gradle.enterprise") version("3.15")
}
gradleEnterprise {
buildScan {
server = "https://your-server.com"
publishAlways()
capture {
taskInputFiles = true
}
}
}
配置好之后,每次构建都会自动上传扫描数据。你可以在Gradle Enterprise的Web界面上,按时间维度分析构建耗时变化。
我在项目中遇到过一种情况:某次代码合并后,构建时间突然从2分钟暴涨到10分钟。打开构建扫描一看,发现是一个自定义任务没有正确配置增量输入,导致每次都要全量执行。嗯,这种问题在扫描报告里一目了然。
配置优化 —— 别让初始化拖后腿
配置阶段是Gradle构建的「起跑线」。很多人忽略它,其实这里坑很多。
为什么会慢?最常见的原因是:在配置阶段执行了耗时操作。
比如下面这种写法,就是典型的反面教材:
// ❌ 错误:在配置阶段下载文件
tasks.register("downloadFile") {
doLast {
// 这里才是执行阶段
}
}
// 配置阶段直接执行
val file = URI("https://example.com/data.zip").toURL().readText()
正确的做法是:把耗时操作放到 doLast 或 doFirst 里,延迟到执行阶段再跑。
// ✅ 正确:延迟执行
tasks.register("downloadFile") {
doLast {
val file = URI("https://example.com/data.zip").toURL().readText()
// 处理文件
}
}
注意:配置阶段不要调用 fileTree、zipTree 等会触发文件系统扫描的方法。我曾经因为这个踩过坑,一个项目配置阶段硬生生跑了30秒。
另外,减少不必要的插件也是配置优化的关键。每个插件都会在配置阶段注册任务和扩展。如果你只用了某个插件的一两个功能,考虑手动实现,而不是全量引入。
任务优化 —— 让增量构建真正生效
任务优化是性能优化的重头戏。Gradle的增量构建机制,说白了就是:输入没变,输出就不重新生成。
但很多自定义任务,并没有正确声明输入输出。导致Gradle无法判断是否需要重新执行。
来看一个例子:
// ❌ 错误:没有声明输入输出
abstract class MyTask : DefaultTask() {
@TaskAction
fun process() {
val input = File("input.txt")
val output = File("output.txt")
output.writeText(input.readText().uppercase())
}
}
这个任务每次都会执行,因为Gradle不知道它的输入是什么。正确的写法是:
// ✅ 正确:声明输入输出
abstract class MyTask : DefaultTask() {
@get:InputFile
abstract val inputFile: RegularFileProperty
@get:OutputFile
abstract val outputFile: RegularFileProperty
@TaskAction
fun process() {
val input = inputFile.get().asFile
val output = outputFile.get().asFile
output.writeText(input.readText().uppercase())
}
}
你想想看,只要输入文件没变,Gradle就会跳过这个任务。对于大型项目,这种优化能节省大量时间。
核心原则:每个自定义任务,都必须用 @Input、@Output 系列注解标注清楚。这是增量构建生效的前提。
知识体系总览
下面这张图,概括了本章的核心逻辑。你可以把它当作性能优化的「检查清单」。
实战建议
最后,我总结几条实战中验证过的经验:
- 先做扫描,再动手:不要凭感觉优化。构建扫描会告诉你真正的瓶颈在哪。
- 配置阶段控制在5秒内:如果超过,检查是否有插件在配置阶段做了IO操作。
- 每个自定义任务都要有输入输出注解:这是增量构建的基石。
- 善用构建缓存:本地缓存 + 远程缓存,能避免重复构建。
- 定期做性能回归:每次大版本升级后,跑一次构建扫描对比。
个人习惯:我会在项目的 gradle.properties 里默认开启构建扫描的自动发布。这样每次CI构建都会留下记录,方便回溯。
嗯,性能优化这件事,说白了就是「测量-分析-优化」的循环。工具给你了,方法也讲了,剩下的就是动手去试。别怕慢,怕的是不知道慢在哪。
公众号:蓝海资料掘金营,微信deep3321