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

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

# 建议

1

更新版本

2

启用并行执行

3

启用 Daemon

4

启用构建缓存

5

启用配置缓存

6

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

7

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

8

增加堆大小

9

优化配置

10

优化依赖解析

11

优化 Java 项目

12

优化 Android 项目

13

提升旧版本 Gradle 的性能

0. 检查你的构建

在进行任何更改之前,使用 构建扫描 (Build Scan)Profile 报告检查你的构建。彻底的检查有助于你了解

  • 总构建时间

  • 构建的哪些部分比较慢

这提供了一个衡量优化效果的基线。

为了最大化本页的价值

  • 检查你的构建。

  • 应用一个更改。

  • 再次检查你的构建。

如果更改缩短了构建时间,则保留它。如果没有,则撤销更改并尝试其他方法。

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

performance 1

使用 Gradle 10.0 构建耗时 21 秒

1. 更新版本

Gradle

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

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

当我们的参考项目更新到使用 Gradle 13.0 时,构建 (./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

并行构建可以显著缩短构建时间,但效果取决于你的项目结构和子项目间的相互依赖关系。如果单个子项目主导了执行时间,或者子项目之间存在许多依赖,那么并行构建的好处将微乎其微。然而,大多数多项目构建都能看到构建时间的显著减少。

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

performance 3

使用构建扫描可视化并行性

构建扫描在“时间线”选项卡中提供任务执行的可视化时间线。

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

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

你可以使用

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

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

    • Develocity 提供了一种用于 CI 和开发者构建的共享缓存解决方案。

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

performance 5

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

使用构建扫描可视化构建缓存

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

  • 与缓存交互的任务数量

  • 使用了哪个缓存

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

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

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

task execution cacheable
图 4. 面向任务的性能视图
timeline not cacheable
图 5. 仅显示“不可缓存”任务的时间线屏幕

要识别优化机会,请在时间线视图中按持续时间对任务进行排序。上面的构建扫描显示,: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

当在我们的参考项目中使用 configuration cache 标志时,构建 (./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

使用构建扫描时间线可视化增量构建

查看构建扫描的“时间线”视图,以识别可以从增量构建中受益的任务。这有助于你理解为什么在你期望 Gradle 跳过任务时,它们却执行了。

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

在上面的示例中,任务不是最新的,因为它的一项输入 (“timestamp”) 发生了变化,强制其重新运行。

要优化你的构建,请按持续时间排序任务,以识别项目中速度最慢的任务。

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 领域对象容器(例如 tasks、configurations、dependencies 和 extensions)上均可用。某些容器(如 TaskContainer)具有 specialized 方法,例如接受任务类型的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 会更频繁地查询仓库。当你不需要每次构建都获取依赖项的最新版本时,考虑删除这些设置的自定义值。

使用构建扫描查找动态和变化中的版本

要定位动态依赖项,请使用构建扫描

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

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

通过使用 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 任务执行时才被解析。

使用构建扫描可视化依赖解析

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

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

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

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

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

使用构建扫描profile 报告来确保自定义依赖解析逻辑没有导致性能问题。此逻辑可能存在于你的构建脚本中,也可能是第三方插件的一部分。

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

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) 引起的意外下载

构建扫描的性能选项卡包含一个网络活动部分,其中包含: - 下载依赖项的总耗时 - 下载传输速率 - 按下载时间排序的依赖项列表

在下面的示例中,两次慢速下载分别耗时 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 语言的项目。

优化测试执行

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

构建扫描可以帮助你识别最慢的测试,并相应地优先进行性能改进。

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

上图显示了来自构建扫描的交互式测试报告,按测试持续时间排序。

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 Scans,它提供更丰富的测试洞察。

要禁用报告,请将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启动成本来显著提高构建性能。自Gradle 3.0起,Daemon已默认启用。

如果您使用的是旧版本,请考虑升级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或更高版本。

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