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()

正确的做法是:把耗时操作放到 doLastdoFirst 里,延迟到执行阶段再跑。

// ✅ 正确:延迟执行
tasks.register("downloadFile") {
    doLast {
        val file = URI("https://example.com/data.zip").toURL().readText()
        // 处理文件
    }
}

注意:配置阶段不要调用 fileTreezipTree 等会触发文件系统扫描的方法。我曾经因为这个踩过坑,一个项目配置阶段硬生生跑了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 系列注解标注清楚。这是增量构建生效的前提。

知识体系总览

下面这张图,概括了本章的核心逻辑。你可以把它当作性能优化的「检查清单」。

Gradle 性能优化知识体系 构建扫描 --scan 参数 构建分析 Enterprise 平台 配置优化 延迟执行 任务优化 增量构建 任务耗时 / 缓存命中 趋势分析 / 回归检测 减少插件 / 延迟计算 @Input / @Output 注解 目标:构建时间缩短 50% 以上 先扫描 → 再分析 → 后优化 → 最后验证 持续集成 → 持续优化

实战建议

最后,我总结几条实战中验证过的经验:

  1. 先做扫描,再动手:不要凭感觉优化。构建扫描会告诉你真正的瓶颈在哪。
  2. 配置阶段控制在5秒内:如果超过,检查是否有插件在配置阶段做了IO操作。
  3. 每个自定义任务都要有输入输出注解:这是增量构建的基石。
  4. 善用构建缓存:本地缓存 + 远程缓存,能避免重复构建。
  5. 定期做性能回归:每次大版本升级后,跑一次构建扫描对比。

个人习惯:我会在项目的 gradle.properties 里默认开启构建扫描的自动发布。这样每次CI构建都会留下记录,方便回溯。

嗯,性能优化这件事,说白了就是「测量-分析-优化」的循环。工具给你了,方法也讲了,剩下的就是动手去试。别怕慢,怕的是不知道慢在哪。


公众号:蓝海资料掘金营,微信deep3321