11.1.6 Kotlin Gradle 插件中的编译与缓存

原文链接: https://kotlinlang.org/docs/gradle-compilation-and-caches.html

11.1.6 Kotlin Gradle 插件中的编译与缓存

在本页中,你可以了解以下主题:

增量编译

Kotlin Gradle 插件支持增量编译,它在 Kotlin/JVM 和 Kotlin/JS 项目中默认启用。增量编译会跟踪两次构建之间 classpath 中文件的变化,从而只编译受这些变化影响的文件。这种方式与 Gradle 的构建缓存配合工作,并支持跳过编译。

对于 Kotlin/JVM,增量编译依赖 classpath 快照,它记录模块的 API 结构,以判断何时需要重新编译。为了优化整个流水线,Kotlin 编译器使用两种类型的 classpath 快照:

  • **细粒度快照:**包含类成员的详细信息,例如属性或函数。当检测到成员级变化时,Kotlin 编译器只重新编译依赖被修改成员的类。为保持性能,Kotlin Gradle 插件会为 Gradle 缓存中的 .jar 文件创建粗粒度快照。
  • **粗粒度快照:**只包含类的 ABI 哈希。当 ABI 的某一部分发生变化时,Kotlin 编译器会重新编译所有依赖被修改类的类。这对于很少变化的类(例如外部库)很有用。

注意: Kotlin/JS 项目使用基于历史文件的不同增量编译方式。

有几种方式可以禁用增量编译:

  • 对于 Kotlin/JVM,设置 kotlin.incremental=false。
  • 对于 Kotlin/JS 项目,设置 kotlin.incremental.js=false。
  • 以命令行参数形式使用 -Pkotlin.incremental=false 或 -Pkotlin.incremental.js=false。

该参数需要添加到后续的每一次构建中。

当你禁用增量编译时,增量缓存在构建后会失效。第一次构建从来都不是增量构建。

提示: 有时增量编译的问题会在故障发生后的若干轮构建之后才显现出来。使用构建报告来跟踪变更和编译的历史。这有助于你提交可复现的缺陷报告。

要进一步了解当前的增量编译方式是如何工作的、以及它与之前方式的对比,请参阅我们的博客文章。

Gradle 构建缓存支持

Kotlin 插件使用 Gradle 构建缓存,它会存储构建输出,以便在未来的构建中复用。

要禁用所有 Kotlin 任务的缓存,请把系统属性 kotlin.caching.enabled 设置为 false(使用参数 -Dkotlin.caching.enabled=false 运行构建)。

Gradle 配置缓存支持

Kotlin 插件使用 Gradle 配置缓存,它通过为后续构建复用配置阶段的结果来加速构建过程。

请参阅 Gradle 文档了解如何启用配置缓存。启用该功能后,Kotlin Gradle 插件会自动开始使用它。

Kotlin 守护进程以及如何在 Gradle 中使用它

Kotlin 守护进程:

  • 与 Gradle 守护进程一起运行来编译项目。
  • 当你使用 IntelliJ IDEA 内置的构建系统编译项目时,它独立于 Gradle 守护进程运行。

Kotlin 守护进程会在某个 Kotlin 编译任务开始编译源码时,在 Gradle 的执行阶段启动。Kotlin 守护进程会随 Gradle 守护进程一起停止,或者在没有 Kotlin 编译的空闲两小时后停止。

Kotlin 守护进程使用的 JDK 与 Gradle 守护进程相同。

设置 Kotlin 守护进程的 JVM 参数

以下每一种设置参数的方式都会覆盖之前的设置:

Gradle 守护进程参数继承

默认情况下,Kotlin 守护进程会从 Gradle 守护进程继承一组特定的参数,但如果为 Kotlin 守护进程直接指定了 JVM 参数,则会覆盖这些继承的参数。例如,如果你在 gradle.properties 文件中添加以下 JVM 参数:

org.gradle.jvmargs=-Xmx1500m -Xms500m -XX:MaxMetaspaceSize=1g

这些参数随后会被添加到 Kotlin 守护进程的 JVM 参数中:

-Xmx1500m -XX:ReservedCodeCacheSize=320m -XX:MaxMetaspaceSize=1g -XX:UseParallelGC -ea -XX:+UseCodeCacheFlushing -XX:+HeapDumpOnOutOfMemoryError -Djava.awt.headless=true -Djava.rmi.server.hostname=127.0.0.1 --add-exports=java.base/sun.nio.ch=ALL-UNNAMED

注意: 要进一步了解 Kotlin 守护进程在 JVM 参数方面的默认行为,请参阅 Kotlin 守护进程在使用 JVM 参数时的行为。

kotlin.daemon.jvm.options 系统属性

如果 Gradle 守护进程的 JVM 参数中包含 kotlin.daemon.jvm.options 系统属性 —— 请在 gradle.properties 文件中使用它:

org.gradle.jvmargs=-Dkotlin.daemon.jvm.options=-Xmx1500m,Xms500m

传递参数时,请遵循以下规则:

  • 仅在参数 Xmx、XX:MaxMetaspaceSize 和 XX:ReservedCodeCacheSize 之前使用减号 -。
  • 用逗号(,)分隔参数,_不要_加空格。空格之后的参数会用于 Gradle 守护进程,而不是 Kotlin 守护进程。

警告: 如果同时满足以下所有条件,Gradle 会忽略这些属性:* Gradle 使用 JDK 1.9 或更高版本。* Gradle 版本在 7.0 到 7.1.1 之间(含两端)。* Gradle 正在编译 Kotlin DSL 脚本。* Kotlin 守护进程没有运行。要解决这个问题,请把 Gradle 升级到 7.2(或更高)版本,或者使用 kotlin.daemon.jvmargs 属性 —— 参见下一节。

kotlin.daemon.jvmargs 属性

你可以在 gradle.properties 文件中添加 kotlin.daemon.jvmargs 属性:

kotlin.daemon.jvmargs=-Xmx1500m -Xms500m

请注意,如果你没有在此处或 Gradle 的 JVM 参数中指定 ReservedCodeCacheSize 参数,Kotlin Gradle 插件会应用默认值 320m:

-Xmx1500m -XX:ReservedCodeCacheSize=320m -Xms500m

kotlin 扩展

你可以在 kotlin 扩展中指定参数:

Kotlin

1
2
3
kotlin {
    kotlinDaemonJvmArgs = listOf("-Xmx486m", "-Xms256m", "-XX:+UseParallelGC")
}

Groovy

1
2
3
kotlin {
    kotlinDaemonJvmArgs = ["-Xmx486m", "-Xms256m", "-XX:+UseParallelGC"]
}

特定任务定义

你可以为特定任务指定参数:

Kotlin

1
2
3
tasks.withType<CompileUsingKotlinDaemon>().configureEach {
    kotlinDaemonJvmArguments.set(listOf("-Xmx486m", "-Xms256m", "-XX:+UseParallelGC"))
}

Groovy

1
2
3
tasks.withType(CompileUsingKotlinDaemon).configureEach { task ->
    task.kotlinDaemonJvmArguments = ["-Xmx1g", "-Xms512m"]
}

注意: 在这种情况下,任务执行时可能会启动一个新的 Kotlin 守护进程实例。进一步了解 Kotlin 守护进程在使用 JVM 参数时的行为。

Kotlin 守护进程在使用 JVM 参数时的行为

在配置 Kotlin 守护进程的 JVM 参数时,请注意:

  • 当不同的子项目或任务使用不同的 JVM 参数集时,预期同时会有多个 Kotlin 守护进程实例在运行。
  • 只有当 Gradle 运行相关的编译任务,且现有的 Kotlin 守护进程没有相同的 JVM 参数集时,才会启动新的 Kotlin 守护进程实例。设想你的项目有很多子项目,其中大多数都需要一定的堆内存来运行 Kotlin 守护进程,但有一个模块需要很多(尽管它很少被编译)。在这种情况下,你应为该模块提供一组不同的 JVM 参数,这样只有涉及这个特定模块的开发者才会启动堆内存更大的 Kotlin 守护进程。

注意: 如果你已经在运行一个堆内存足以处理该编译请求的 Kotlin 守护进程,那么即使其他请求的 JVM 参数不同,也会复用该守护进程,而不会启动新的实例。

如果没有指定以下参数,Kotlin 守护进程会从 Gradle 守护进程继承它们:

  • -Xmx
  • -XX:MaxMetaspaceSize
  • -XX:ReservedCodeCacheSize。如果未指定也未继承,默认值是 320m。

Kotlin 守护进程具有以下默认 JVM 参数:

  • -XX:UseParallelGC。只有在未指定其他垃圾回收器时才会应用该参数。
  • -ea
  • -XX:+UseCodeCacheFlushing
  • -Djava.awt.headless=true
  • -D{java.servername.property}={localhostip}
  • --add-exports=java.base/sun.nio.ch=ALL-UNNAMED。该参数只对 JDK 16 或更高版本应用。

注意: Kotlin 守护进程的默认 JVM 参数列表可能因版本而异。你可以使用 VisualVM 之类的工具检查正在运行的 JVM 进程(例如 Kotlin 守护进程)的实际设置。

回退到之前的编译器

从 Kotlin 2.0.0 开始,默认使用 K2 编译器。

要在 Kotlin 2.0.0 及更高版本中使用之前的编译器,可以:

或者

  • 使用以下编译器选项:-language-version 1.9。

要进一步了解 K2 编译器的优势,请参阅 K2 编译器迁移指南。

尝试最新的语言版本

从 Kotlin 2.0.0 开始,要试用最新的语言版本,请在 gradle.properties 文件中设置 kotlin.experimental.tryNext 属性。使用该属性时,Kotlin Gradle 插件会把语言版本提升到你的 Kotlin 版本默认值之上的一个版本。例如,在 Kotlin 2.0.0 中,默认语言版本是 2.0,因此该属性会配置语言版本 2.1。

或者,你也可以运行以下命令:

1
./gradlew assemble -Pkotlin.experimental.tryNext=true

在构建报告中,你可以找到用于编译每个任务的语言版本。

构建报告

构建报告包含各个编译阶段的耗时,以及编译无法增量进行的原因。当编译时间过长,或者同一个项目的编译时间出现差异时,可以使用构建报告来调查性能问题。

与以单个 Gradle 任务为粒度单位的 Gradle 构建扫描相比,Kotlin 构建报告能帮助你更高效地调查构建性能问题。

分析耗时较长的编译的构建报告,可以帮助你解决两种常见情况:

  • 构建不是增量的。分析原因并修复背后的根本问题。
  • 构建是增量的,但耗时过长。尝试重新组织源文件——拆分大文件、把不同的类保存到不同文件中、重构大型类、把顶层函数声明在不同文件中等等。

构建报告还会显示项目中使用的 Kotlin 版本。此外,从 Kotlin 1.9.0 开始,你可以在 Gradle 构建扫描中看到编译代码时使用的是哪个编译器。

了解如何阅读构建报告,以及 JetBrains 如何使用构建报告。

启用构建报告

要启用构建报告,请在 gradle.properties 中声明构建报告输出的保存位置:

kotlin.build.report.output=file

输出可用以下值及其组合:

| 选项 | 说明 |

| file | 把构建报告以人类可读的格式保存到本地文件。默认位置是 ${project_folder}/build/reports/kotlin-build/${project_name}-timestamp.txt | | single_file | 把构建报告以对象格式保存到指定的本地文件。 | | build_scan | 把构建报告保存到构建扫描的 custom values 部分。请注意,Gradle Enterprise 插件会限制自定义值的数量及其长度。在大型项目中,某些值可能会丢失。 | | http | 通过 HTTP(S) 提交构建报告。POST 方法以 JSON 格式发送指标。你可以在 Kotlin 仓库中看到所发送数据的当前版本。你可以在这篇博客文章中找到 HTTP 端点的示例 | | json | 把构建报告以 JSON 格式保存到本地文件。默认位置是 ${project_folder}/build/reports/kotlin-build/${project_name}-build-<date-time>-<index>.json。 |

以下是 kotlin.build.report 可用的选项列表:

# Required outputs. Any combination is allowed
kotlin.build.report.output=file,single_file,http,build_scan,json

# Mandatory if single_file output is used. Where to put reports
# Use instead of the deprecated `kotlin.internal.single.build.metrics.file` property
kotlin.build.report.single_file=my/directory/path/some_filename

# Optional. Output directory for file-based or JSON reports. Default: build/reports/kotlin-build/
kotlin.build.report.file.output_dir=kotlin-reports

# Optional. Label for marking your build report (for example, debug parameters)
kotlin.build.report.label=some_label

仅适用于 HTTP 的选项:

# Mandatory. Where to post HTTP(S)-based reports
kotlin.build.report.http.url=http://127.0.0.1:8080

# Optional. User and password if the HTTP endpoint requires authentication
kotlin.build.report.http.user=someUser
kotlin.build.report.http.password=somePassword

# Optional. Add a Git branch name of a build to a build report
kotlin.build.report.http.include_git_branch.name=true|false

# Optional. Add compiler arguments to a build report
# If a project contains many modules, its compiler arguments in the report can be very heavy and not that helpful
kotlin.build.report.include_compiler_arguments=true|false

自定义值的数量限制

为了收集构建扫描的统计数据,Kotlin 构建报告使用 Gradle 的自定义值。你和不同的 Gradle 插件都可以把数据写入自定义值。自定义值的数量是有上限的。请在构建扫描插件文档中查看当前的自定义值上限。

如果你的项目很大,这类自定义值的数量可能相当多。如果数量超过上限,你会在日志中看到以下消息:

1
Maximum number of custom values (1,000) exceeded

要减少 Kotlin 插件产生的自定义值数量,你可以在 gradle.properties 中使用以下属性:

kotlin.build.report.build_scan.custom_values_limit=500

关闭项目和系统属性收集

HTTP 构建统计数据日志可能包含一些项目和系统属性。这些属性可能改变构建行为,因此把它们记录到构建统计数据中很有用。这些属性可能存储敏感数据,例如密码或项目的完整路径。

你可以在 gradle.properties 中添加 kotlin.build.report.http.verbose_environment 属性来禁止收集这些统计数据。

注意: JetBrains 不会收集这些统计数据。由你选择将报告存储在哪里。

接下来做什么?

进一步了解: