构建性能对生产力至关重要。构建时间越长,对开发流程的干扰就越大。由于构建每天运行多次,即使是很小的延迟也会累积起来。持续集成 (CI) 也是如此。

投资于构建速度是值得的。本节探讨了优化性能的方法,强调了常见的陷阱,并解释了如何避免它们。

# 建议

1

更新版本

2

启用并行执行

3

启用 Daemon

4

启用构建缓存

5

启用配置缓存

6

为自定义任务启用增量构建

7

为特定开发工作流创建构建

8

增加堆大小

9

优化配置

10

优化依赖解析

11

优化 Java 项目

12

优化 Android 项目

13

改进旧版本 Gradle

0. 检查你的构建

在进行任何更改之前,使用 Build Scan配置文件报告检查你的构建。彻底的检查有助于你了解

  • 总构建时间

  • 构建的哪些部分很慢

这提供了一个衡量优化影响的基线。

为了从本页获得最大价值

  • 检查你的构建。

  • 应用更改。

  • 再次检查你的构建。

如果更改改善了构建时间,请保留它。如果没有,请撤销更改并尝试另一种方法。

作为参考,以下 Build Scan 快照是使用 gradle init 创建的项目构建。它是一个使用 Kotlin 构建文件的 Java (JDK 21) Application and library project

performance 1

它使用 Gradle 8.10 在 21 秒内构建完成。

1. 更新版本

Gradle

每个 Gradle 版本都带来性能改进。使用过时的版本意味着错失这些收益。升级风险较低,因为 Gradle 在次要版本之间保持向后兼容性。及时更新还可以通过提供早期废弃警告,使主要版本升级更顺畅。

你可以使用 Gradle Wrapper 通过运行 gradle wrapper --gradle-version X.X 来更新 Gradle 版本,其中 X.X 是所需版本。

当我们的参考项目更新到使用 Gradle 8.13 时,构建 (./gradlew clean build) 时间为 8 秒

performance 2

Java

Gradle 运行在 Java 虚拟机 (JVM) 上,Java 更新通常会提高性能。为了获得最佳的 Gradle 性能,请使用最新的 Java 版本。

不要忘记查看兼容性指南,以确保你的 Java 版本与你的 Gradle 版本兼容。

插件

插件在构建性能中起着关键作用。过时的插件会减慢你的构建速度,而新版本通常包含优化。对于 Android、Java 和 Kotlin 插件尤其如此。保持它们最新以获得最佳性能。

只需查看项目中所有声明的插件,并检查是否有新版本可用

plugins {
    id("org.jlleitschuh.gradle.ktlint") version "12.0.0" // A newer version is available on the Gradle Plugin Portal
}

2. 启用并行执行

大多数项目由多个子项目组成,其中一些是独立的。然而,默认情况下,Gradle 一次只运行一个任务。

要并行执行来自不同子项目的任务,请使用 --parallel 标志

$ gradle <task> --parallel

要默认启用并行执行,请将此设置添加到 项目根目录或你的 Gradle 主目录中的 gradle.properties

gradle.properties
org.gradle.parallel=true

并行构建可以显著提高构建时间,但影响取决于你的项目结构和子项目间依赖关系。如果单个子项目占据了大部分执行时间,或者子项目之间存在大量依赖关系,则收益将微乎其微。然而,大多数多项目构建会看到构建时间显著减少。

当在我们的参考项目上使用并行标志时,构建 (./gradlew clean build --parallel) 时间为 7 秒

performance 3

使用 Build Scan 可视化并行性

Build Scan 在“时间轴”选项卡中提供了任务执行的视觉时间轴。

在下面的示例中,构建最初在开始和结束时有长时间运行的任务,造成了瓶颈

parallel task slow
图 1. 并行执行中的瓶颈

通过调整构建配置以使这两个慢任务更早地并行运行,总构建时间从 8 秒减少到 5 秒

parallel task fast
图 2. 优化后的并行执行

3. 重新启用 Gradle Daemon

Gradle Daemon 通过以下方式显著缩短构建时间

  • 跨构建缓存项目信息

  • 在后台运行以避免 JVM 启动延迟

  • 受益于持续的 JVM 运行时优化

  • 监视文件系统以确定需要重建的内容

Gradle 默认启用 Daemon,但某些构建会覆盖此设置。如果你的构建禁用了它,启用 Daemon 可以带来显著的性能改进。

要在构建时启用 Daemon,请使用

$ gradle <task> --daemon

对于旧版本 Gradle,通过将此添加到 gradle.properties 永久启用它

gradle.properties
org.gradle.daemon=true

在开发机器上,启用 Daemon 可以提高性能。在 CI 机器上,长期运行的代理会受益,但短期运行的代理可能不会。自 Gradle 3.0 以来,Daemon 在内存压力下会自动关闭,因此保持 Daemon 启用是安全的。

当在我们的参考项目上使用 Daemon 时,构建 (./gradlew clean build --daemon) 时间为 3 秒

performance 4

4. 启用构建缓存

Gradle 构建缓存通过存储特定输入的任务输出来优化性能。如果任务使用相同的输入再次运行,Gradle 会检索缓存的输出而不是重新执行任务。

默认情况下,Gradle 不使用构建缓存。要在构建时启用它,请使用

$ gradle <task> --build-cache

要永久启用它,请将其添加到 gradle.properties

gradle.properties
org.gradle.caching=true

你可以使用

  • 本地构建缓存以加快同一台机器上重复的构建。

  • 共享构建缓存以加速跨多台机器的构建。

当在我们的参考项目上使用构建缓存标志时,构建 (./gradlew clean build --build-cache) 时间为 5 秒

performance 5

有关构建缓存的更多信息,请查阅 构建缓存文档

使用 Build Scan 可视化构建缓存

Build Scan 通过“性能”页面中的“构建缓存”选项卡帮助你分析构建缓存效率。此选项卡提供关键统计信息,包括

  • 与缓存交互的任务数量

  • 使用了哪个缓存

  • 缓存条目的传输和打包/解包速率

cache performance
图 3. 检查构建缓存的性能

“任务执行”选项卡提供了对任务可缓存性的见解。单击类别会显示一个突出显示该类别任务的时间轴

task execution cacheable
图 4. 以任务为中心的性能视图
timeline not cacheable
图 5. 仅包含“不可缓存”任务的时间轴屏幕

要确定优化机会,请在时间轴视图中按持续时间对任务进行排序。上面的 Build Scan 显示 :task1:task3 可以改进并使其可缓存,同时也解释了 Gradle 为何没有缓存它们。

5. 启用配置缓存

此功能有以下限制

  • 并非所有 核心 Gradle 插件功能都受支持。全面支持仍在进行中。

  • 你的构建及其插件可能需要调整以满足要求

  • IDE 导入和同步不使用配置缓存。

配置缓存通过缓存配置阶段的结果来加速构建。当构建配置输入保持不变时,Gradle 可以完全跳过此阶段。

启用配置缓存提供了进一步的性能优势。启用后,Gradle

  • 并行执行所有任务,即使在同一子项目内。

  • 缓存依赖解析结果以避免冗余计算。

构建配置输入包括

  • 初始化脚本

  • 设置脚本

  • 构建脚本

  • 配置期间使用的系统和 Gradle 属性

  • 配置期间使用的环境变量

  • 通过值提供者 (providers) 访问的配置文件

  • buildSrc 输入,包括配置文件和源文件

默认情况下,Gradle 不使用配置缓存。要在构建时启用它,请使用

$ gradle <task> --configuration-cache

要永久启用它,请将此设置添加到 gradle.properties 文件中

gradle.properties
org.gradle.configuration-cache=true

当在我们的参考项目上使用配置缓存标志时,构建 (./gradlew clean build --build-cache) 时间为 4 秒

performance 6

有关更多详细信息,请参阅配置缓存文档

6. 为自定义任务启用增量构建

增量构建是 Gradle 的一项优化,它会跳过已使用相同输入执行的任务。如果任务的输入和输出自上次执行以来没有更改,Gradle 将跳过该任务。

大多数内置的 Gradle 任务都支持增量构建。要使自定义任务兼容,你必须指定其输入和输出

build.gradle.kts
tasks.register("processTemplatesAdHoc") {
    inputs.property("engine", TemplateEngineType.FREEMARKER)
    inputs.files(fileTree("src/templates"))
        .withPropertyName("sourceFiles")
        .withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.property("templateData.name", "docs")
    inputs.property("templateData.variables", mapOf("year" to "2013"))
    outputs.dir(layout.buildDirectory.dir("genOutput2"))
        .withPropertyName("outputDir")

    doLast {
        // Process the templates here
    }
}
build.gradle
tasks.register('processTemplatesAdHoc') {
    inputs.property('engine', TemplateEngineType.FREEMARKER)
    inputs.files(fileTree('src/templates'))
        .withPropertyName('sourceFiles')
        .withPathSensitivity(PathSensitivity.RELATIVE)
    inputs.property('templateData.name', 'docs')
    inputs.property('templateData.variables', [year: '2013'])
    outputs.dir(layout.buildDirectory.dir('genOutput2'))
        .withPropertyName('outputDir')

    doLast {
        // Process the templates here
    }
}

有关更多详细信息,请参阅增量构建文档编写任务教程

当在我们的参考项目上利用增量构建时,构建 (./gradlew clean build build) 时间为 5 秒

performance 7

使用 Build Scan 时间轴可视化增量构建

查看 Build Scan “时间轴”视图以识别可以从增量构建中受益的任务。这有助于你了解当期望 Gradle 跳过任务时,任务为何会执行。

timeline
图 6. 时间轴视图有助于增量构建检查

在上面的示例中,任务不是最新的,因为其输入之一(“时间戳”)已更改,强制其重新运行。

为了优化你的构建,按持续时间对任务进行排序以识别项目中运行最慢的任务。

7. 为特定开发工作流创建构建

最快的任务是那些不运行的任务。通过跳过不必要的任务,你可以显著提高构建性能。

如果你的构建包含多个子项目,请定义独立构建它们的任务。这最大限度地提高了缓存效率,并防止一个子项目中的更改触发其他子项目中不必要的重建。它还有助于在不同子项目上工作的团队避免冗余构建——例如

  • 前端开发人员不需要每次修改前端时都构建后端子项目。

  • 文档编写人员不需要构建前端或后端代码,即使文档在同一个项目中。

相反,创建开发人员特定任务,同时为整个项目维护单个任务图。每组用户都需要任务的一个子集——将该子集转换为排除不必要任务的 Gradle 工作流。

Gradle 提供了几个功能来创建高效的工作流

  • 将任务分配到适当的

  • 创建聚合任务——没有操作但依赖于其他任务的任务(例如,assemble)。

  • 使用 gradle.taskGraph.whenReady() 延迟配置,仅在必要时执行验证。

8. 增加堆大小

默认情况下,Gradle 为你的构建保留 512MB 的堆空间,这对于大多数项目来说是足够的。

然而,非常大的构建可能需要更多的内存来存储 Gradle 的模型和缓存。如果需要,你可以在项目根目录或你的 Gradle 主目录中的 gradle.properties 文件中指定以下属性来增加堆大小

gradle.properties
org.gradle.jvmargs=-Xmx2048M

有关更多详细信息,请参阅JVM 内存配置文档。

9. 优化配置

构建生命周期章节所述,Gradle 构建经历三个阶段:初始化、配置和执行配置阶段总是执行,无论运行哪些任务。此阶段的任何昂贵操作都会减慢每次构建,包括简单的命令,如 gradle helpgradle tasks

以下各节介绍了减少配置阶段所花费时间的技术。

你还可以启用配置缓存,以最大程度地减少慢速配置阶段的影响。然而,即使有缓存,配置阶段仍然偶尔会运行。优化它仍然至关重要。

避免昂贵或阻塞的工作

在配置阶段应避免耗时的工作。然而,它有时会意外地潜入。

虽然在构建脚本中加密数据或进行远程服务调用是显而易见的,但这种逻辑通常隐藏在插件或自定义任务类中。插件的 apply() 方法或任务的构造函数中的昂贵操作是危险信号

class ExpensivePlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        // ❌ BAD: Makes an expensive network call at configuration time
        def response = new URL("https://example.com/dependencies.json").text
        def dependencies = new groovy.json.JsonSlurper().parseText(response)

        dependencies.each { dep ->
            project.dependencies.add("implementation", dep)
        }
    }
}

相反

class OptimizedPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.tasks.register("fetchDependencies") {
            doLast {
                // ✅ GOOD: Runs only when the task is executed
                def response = new URL("https://example.com/dependencies.json").text
                def dependencies = new groovy.json.JsonSlurper().parseText(response)

                dependencies.each { dep ->
                    project.dependencies.add("implementation", dep)
                }
            }
        }
    }
}

仅在需要时应用插件

每个应用的插件或脚本都会增加配置时间,有些插件的影响比其他插件更大。与其完全避免插件,不如确保它们仅在必要时应用。例如,使用 allprojects {}subprojects {} 可以将插件应用于所有子项目,即使并非所有子项目都需要它们。

在下面的示例中,根构建脚本将 script-a.gradle 应用于三个子项目

subprojects {
    apply from: "$rootDir/script-a.gradle"  // ❌ Applied to all subprojects unnecessarily
}
script a application
图 7. 显示将 script-a.gradle 应用于构建

该脚本每个子项目运行 1 秒,总共延迟配置阶段 3 秒。为了优化这一点

  • 如果只有一个子项目需要该脚本,请将其从其他子项目中移除,从而将配置延迟减少 2 秒

    project(":subproject1") {
        apply from: "$rootDir/script-a.gradle"  // ✅ Applied only where needed
    }
    
    project(":subproject2") {
        apply from: "$rootDir/script-a.gradle"
    }
  • 如果多个(但不是所有)子项目使用该脚本,请将其重构为 buildSrc 中的自定义插件,并仅将其应用于相关子项目。这可以减少配置时间并避免代码重复。

    plugins {
        id 'com.example.my-custom-plugin' apply false  // ✅ Declare the plugin but don't apply it globally
    }
    
    project(":subproject1") {
        apply plugin: 'com.example.my-custom-plugin'  // ✅ Apply only where needed
    }
    
    project(":subproject2") {
        apply plugin: 'com.example.my-custom-plugin'
    }

静态编译任务和插件

许多 Gradle 插件和任务都是用 Groovy 编写的,因为它语法简洁、功能 API 丰富且扩展功能强大。然而,Groovy 的动态解释使得方法调用比 Java 或 Kotlin 慢。

你可以通过使用静态 Groovy 编译来降低此成本。在不需要动态功能的 Groovy 类上添加 @CompileStatic 注解。如果方法需要动态行为,请在该方法上使用 @CompileDynamic

或者,考虑用Java 或 Kotlin编写插件和任务,它们默认是静态编译的。

Gradle 的 Groovy DSL 依赖于 Groovy 的动态功能。要在插件中使用静态编译,请采用更像 Java 的语法。

下面的示例定义了一个不带动态功能的复制文件任务

src/main/groovy/MyPlugin.groovy
project.tasks.register('copyFiles', Copy) { Task t ->
    t.into(project.layout.buildDirectory.dir('output'))
    t.from(project.configurations.getByName('compile'))
}

此示例使用 register()getByName(),它们在所有 Gradle 领域对象容器上都可用,例如任务、配置、依赖项和扩展。某些容器,如 TaskContainer,具有专门的方法,例如接受任务类型的create

使用静态编译可以通过以下方式改进 IDE 支持

  • 更快地检测无法识别的类型、属性和方法

  • 更可靠的方法名称自动补全

10. 优化依赖解析

依赖解析简化了将第三方库集成到你的项目中。Gradle 会联系远程服务器以发现和下载依赖项。你可以优化依赖项的引用方式,以最大程度地减少这些远程调用。

避免不必要和未使用的依赖项

管理第三方库及其传递依赖项会增加大量的维护和构建时间成本。重构后,未使用的依赖项通常会保留下来。

如果你只使用库的一小部分,请考虑: - 自己实现所需的功能。 - 如果库是开源的,复制必要的代码(并注明出处)。

优化仓库顺序

Gradle 按照声明的顺序搜索仓库。为了加快解析速度,请首先列出托管大多数依赖项的仓库,从而减少不必要的网络请求。

repositories {
    mavenCentral()  // ❌ Declared first, but most dependencies are in JitPack
    maven { url "https://jitpack.io" }
}

最小化仓库数量

将仓库数量限制在所需的最小数量。

如果使用自定义仓库,请创建一个虚拟仓库,它聚合多个仓库,然后只将该仓库添加到你的构建中。

repositories {
    maven { url "https://repo.mycompany.com/virtual-repo" } // ✅ Uses an aggregated repository
}

最小化动态和快照版本

动态 ("2.+") 和快照版本 ("-SNAPSHOT") 会导致 Gradle 频繁检查远程仓库。默认情况下,Gradle 将动态版本缓存 24 小时,但这可以通过 cacheDynamicVersionsForcacheChangingModulesFor 属性进行配置

configurations.all {
    resolutionStrategy {
        cacheDynamicVersionsFor 4, 'hours'
        cacheChangingModulesFor 10, 'minutes'
    }
}

如果构建文件或初始化脚本降低了这些值,Gradle 会更频繁地查询仓库。当你不需要每次构建都获取依赖项的最新发布版本时,请考虑删除这些设置的自定义值。

使用 Build Scan 查找动态和变化的版本

要定位动态依赖项,请使用 Build Scan

dependency dynamic versions
图 8. 查找具有动态版本的依赖项

在可能的情况下,用 "1.2""3.0.3.GA" 等固定版本替换动态版本,以获得更好的缓存效果。

避免在配置期间解析依赖项

依赖解析是一个 I/O 密集型过程。Gradle 会缓存结果,但在配置阶段触发解析会给每次构建增加不必要的开销。

此代码在配置期间强制进行依赖解析,从而减慢了每次构建的速度

task printDeps {
    doFirst {
        configurations.compileClasspath.files.each { println it } // ✅ Deferring Dependency Resolution
    }
    doLast {
        configurations.compileClasspath.files.each { println it } // ❌ Resolving Dependencies During Configuration
    }
}

切换到声明式语法

配置阶段评估配置文件会强制 Gradle 过早地解析依赖项,从而增加构建时间。通常,任务应仅在执行期间需要时才解析依赖项。

考虑一个调试场景,你想要打印配置中的所有文件。一个常见的错误是直接在构建脚本中打印它们

build.gradle.kts
tasks.register<Copy>("copyFiles") {
    println(">> Compilation deps: ${configurations.compileClasspath.get().files.map { it.name }}")
    into(layout.buildDirectory.dir("output"))
    from(configurations.compileClasspath)
}
build.gradle
tasks.register('copyFiles', Copy) {
    println ">> Compilation deps: ${configurations.compileClasspath.files.name}"
    into(layout.buildDirectory.dir('output'))
    from(configurations.compileClasspath)
}

即使 printDeps 从未执行,files 属性也会立即触发依赖解析。由于配置阶段在每次构建时都会运行,这会减慢所有构建的速度。

通过使用 doFirst(),Gradle 会将依赖解析推迟到任务实际运行时,从而防止在配置阶段进行不必要的工作

build.gradle.kts
tasks.register<Copy>("copyFiles") {
    into(layout.buildDirectory.dir("output"))
    // Store the configuration into a variable because referencing the project from the task action
    // is not compatible with the configuration cache.
    val compileClasspath: FileCollection = configurations.compileClasspath.get()
    from(compileClasspath)
    doFirst {
        println(">> Compilation deps: ${compileClasspath.files.map { it.name }}")
    }
}
build.gradle
tasks.register('copyFiles', Copy) {
    into(layout.buildDirectory.dir('output'))
    // Store the configuration into a variable because referencing the project from the task action
    // is not compatible with the configuration cache.
    FileCollection compileClasspath = configurations.compileClasspath
    from(compileClasspath)
    doFirst {
        println ">> Compilation deps: ${compileClasspath.files.name}"
    }
}

Gradle 的 Copy 任务中的 from() 方法不会立即触发依赖解析,因为它引用的是依赖配置,而不是解析后的文件。这确保了仅当 Copy 任务执行时才解析依赖项。

使用 Build Scan 可视化依赖解析

Build Scan 性能页面上的“依赖解析”选项卡显示了配置和执行阶段的依赖解析时间

bad dependency resolution
图 9. 配置时的依赖解析

Build Scan 提供了识别此问题的另一种方法。你的构建在“项目配置”期间应该花费 0 秒来解析依赖项。此示例显示构建在生命周期中过早地解析了依赖项。你还可以在“性能”页面上找到一个“设置和建议”选项卡。它显示了在配置阶段解析的依赖项。

移除或改进自定义依赖解析逻辑

Gradle 允许用户以灵活的方式建模依赖解析。简单的自定义,例如强制特定版本或替换依赖项,对解析时间的影响最小。然而,复杂的自定义逻辑——例如手动下载和解析 POM 文件——可以显著减慢依赖解析。

使用 Build Scan配置文件报告来确保自定义依赖解析逻辑不会导致性能问题。此逻辑可能存在于你的构建脚本中或作为第三方插件的一部分。

此示例强制使用自定义依赖版本,但也引入了昂贵的逻辑,从而减慢了解析速度

configurations.all {
    resolutionStrategy.eachDependency { details ->
        if (details.requested.group == "com.example" && details.requested.name == "library") {
            def versionInfo = new URL("https://example.com/version-check").text  // ❌ Remote call during resolution
            details.useVersion(versionInfo.trim())  // ❌ Dynamically setting a version based on an HTTP response
        }
    }
}

与其动态获取依赖版本,不如在版本目录中定义它们

dependencies {
    implementation "com.example:library:${versions.libraryVersion}"
}

移除缓慢或意外的依赖下载

缓慢的依赖下载会显著影响构建性能。常见原因包括

  • 慢速互联网连接

  • 过载或距离远的仓库服务器

  • 动态版本 (2.+) 或快照版本 (-SNAPSHOT) 引起的意外下载

Build Scan 中的性能选项卡包含一个网络活动部分,其中包含: - 下载依赖项所花费的总时间 - 下载传输速率 - 按下载时间排序的依赖项列表

在下面的示例中,两次缓慢的下载分别花费了 20 秒40 秒,影响了整体构建时间

slow dependency downloads
图 10. 识别缓慢的依赖下载

检查下载的依赖项列表,查看是否有意外的依赖项。例如,动态版本 (1.+) 可能会触发频繁的远程查找。

为了消除不必要的下载

  • 使用更近或更快的仓库 如果从 Maven Central 下载缓慢,请考虑使用地理位置更近的镜像或内部仓库代理。

  • 从动态版本切换到固定版本

dependencies {
    implementation "com.example:library:1.+" // ❌ Bad
    implementation "com.example:library:1.2.3" // ✅ Good
}

11. 优化 Java 项目

以下各节适用于使用 java 插件或其他 JVM 语言的项目。

优化测试执行

测试通常占构建时间的很大一部分。这些可能包括单元测试和集成测试,其中集成测试通常运行时间更长。

一个Build Scan 可以帮助你识别最慢的测试并相应地优先进行性能改进。

tests longest
图 11. 测试屏幕,按项目显示测试,按持续时间排序

上图显示了 Build Scan 中的交互式测试报告,按测试持续时间排序。

Gradle 提供了几种加速测试执行的策略

  • A. 并行运行测试

  • B. 将测试分叉到多个进程中

  • C. 在不需要时禁用测试报告

让我们仔细看看每个选项。

A. 并行运行测试

Gradle 可以并行运行多个测试类或方法。要启用并行执行,请在你的 Test 任务上设置 maxParallelForks 属性。

一个好的默认值是可用 CPU 核心数或略少于该数量

build.gradle.kts
tasks.withType<Test>().configureEach {
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
}
build.gradle
tasks.withType(Test).configureEach {
    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
}

并行测试执行假设测试是隔离的。避免共享文件系统、数据库或外部服务等共享资源。共享状态或资源的测试可能会由于竞态条件或资源冲突而间歇性失败。

B. 将测试分叉到多个进程中

默认情况下,Gradle 在单个分叉的 JVM 进程中运行所有测试。这对于小型测试套件是高效的,但大型或内存密集型测试套件可能会遭受长时间的执行时间和 GC 暂停。

你可以通过使用 forkEvery 设置在指定数量的测试后分叉新的 JVM 来减少内存压力并隔离有问题的测试

build.gradle.kts
tasks.withType<Test>().configureEach {
    forkEvery = 100
}
build.gradle
tasks.withType(Test).configureEach {
    forkEvery = 100
}
分叉 JVM 是一项昂贵的操作。将 forkEvery 设置得太低会因过多的进程启动开销而增加测试时间。

C. 禁用测试报告

Gradle 默认生成 HTML 和 JUnit XML 测试报告,即使你无意查看它们。报告生成会增加开销,尤其是在大型测试套件中。

如果出现以下情况,你可以完全禁用报告生成

  • 你只需要知道测试是否通过。

  • 你使用 Build Scan,它提供更丰富的测试洞察。

要禁用报告,请将 reports.html.requiredreports.junitXml.required 设置为 false

build.gradle.kts
tasks.withType<Test>().configureEach {
    reports.html.required = false
    reports.junitXml.required = false
}
build.gradle
tasks.withType(Test).configureEach {
    reports.html.required = false
    reports.junitXml.required = false
}
有条件地启用报告

如果你偶尔需要报告而无需修改构建文件,则可以使报告生成依赖于项目属性。

此示例禁用了报告,除非存在 createReports 属性

build.gradle.kts
tasks.withType<Test>().configureEach {
    if (!project.hasProperty("createReports")) {
        reports.html.required = false
        reports.junitXml.required = false
    }
}
build.gradle
tasks.withType(Test).configureEach {
    if (!project.hasProperty("createReports")) {
        reports.html.required = false
        reports.junitXml.required = false
    }
}

要生成报告,请通过命令行传递属性

$ gradle <task> -PcreateReports

或在项目根目录或你的Gradle 用户主目录中的 gradle.properties 文件中定义属性

gradle.properties
createReports=true

优化编译器

Java 编译器速度很快,但在包含数百或数千个类的大型项目中,编译时间仍然会变得很长。

Gradle 提供了几种优化 Java 编译的方法

  • A. 在单独的进程中运行编译器

  • B. 对内部依赖项使用 implementation 可见性

A. 将编译器作为单独的进程运行

默认情况下,Gradle 在与构建逻辑相同的进程中运行编译。你可以使用 fork 选项将 Java 编译卸载到单独的进程中

build.gradle.kts
<task>.options.isFork = true
build.gradle
<task>.options.fork = true

要将此设置应用于所有 JavaCompile 任务,请使用 configureEach

build.gradle.kts
tasks.withType<JavaCompile>().configureEach {
    options.isFork = true
}
build.gradle
tasks.withType(JavaCompile).configureEach {
    options.fork = true
}

Gradle 会在构建期间重用分叉的进程,因此启动成本很低。在自己的 JVM 中运行编译有助于减少主 Gradle 进程中的垃圾回收,这可以加速构建的其余部分——尤其是在与并行执行结合使用时。

分叉编译对小型构建影响不大,但当单个任务编译超过一千个源文件时,它可以显著帮助。

B. 对内部依赖项使用 implementation

在 Gradle 3.4 及更高版本中,你可以对应暴露给下游项目的依赖项使用 api,对内部依赖项使用 implementation。这种区别减少了大型多项目构建中不必要的重新编译。

只有应用了java-library 插件的项目才能使用 apiimplementation 配置。仅使用 java 插件的项目不能声明 api 依赖项。

implementation 依赖项更改时,Gradle 不会重新编译下游消费者——只有当 api 依赖项更改时才会。这有助于减少级联重新编译

build.gradle.kts
dependencies {
   api(project("my-utils"))
   implementation("com.google.guava:guava:21.0")
}
build.gradle
dependencies {
   api project('my-utils')
   implementation 'com.google.guava:guava:21.0'
}

将仅内部依赖项切换到 implementation 是你在大型模块化代码库中提高构建性能最具影响力的更改之一。

12. 优化 Android 项目

本指南中描述的所有性能策略也适用于 Android 构建,因为 Android 项目在底层使用 Gradle。

然而,Android 引入了其独特的优化挑战和机会——尤其是在资源处理、APK 创建和构建变体方面。

有关 Android 特定的其他提示,请查看 Android 团队的官方资源

13. 提高旧版本 Gradle 的性能

我们建议使用最新的 Gradle 版本以受益于最新的性能改进、错误修复和功能。然而,我们理解有些项目——特别是长期存在或遗留的代码库——可能无法轻松升级。

如果你使用的是旧版本的 Gradle,请考虑以下优化以提高构建性能。

启用 Daemon

Gradle Daemon 通过避免构建之间 JVM 启动成本来显著提高构建性能。Daemon 自 Gradle 3.0 起已默认启用。

如果你使用的是旧版本,请考虑升级 Gradle。如果升级不是一个选项,你可以手动启用 Daemon

启用增量编译

Gradle 可以分析类依赖关系,并仅重新编译代码中受更改影响的部分。

增量编译在 Gradle 4.10 及更高版本中默认启用。要在旧版本中手动启用它,请将以下配置添加到你的 build.gradle 文件中

build.gradle.kts
tasks.withType<JavaCompile>().configureEach {
    options.isIncremental = true
}
build.gradle
tasks.withType(JavaCompile).configureEach {
    options.incremental = true
}

使用编译规避

许多代码更改,例如方法体的编辑,都是 ABI 兼容的——它们不影响类的公共 API。Gradle 3.4 及更高版本可以检测这些更改并避免重新编译下游项目,从而显著减少大型多项目构建中的构建时间。

要从编译规避中受益,请升级到 Gradle 3.4 或更高版本。

如果你的项目使用注解处理器,你必须显式声明它们才能充分利用编译规避。有关更多详细信息,请参阅编译规避文档