介绍增量构建支持
任务输入、输出和依赖 #
内置任务,例如 JavaCompile 声明了一组输入(Java 源代码文件)和一组输出(class 文件)。Gradle 使用这些信息来确定任务是否是最新的,是否需要执行任何工作。如果输入或输出都没有更改,Gradle 可以跳过该任务。总而言之,我们将此行为称为 Gradle 的增量构建支持。
要利用增量构建支持,您需要向 Gradle 提供有关任务输入和输出的信息。可以配置任务使其仅具有输出。在执行任务之前,Gradle 会检查输出,如果输出没有更改,则会跳过任务的执行。在实际构建中,任务通常也具有输入——包括源代码文件、资源和属性。Gradle 检查输入和输出是否都未更改,然后才执行任务。
通常,一个任务的输出将作为另一个任务的输入。重要的是要正确设置这些任务之间的顺序,否则任务将以错误的顺序运行或根本不运行。Gradle 不依赖于任务在构建脚本中定义的顺序。新任务是无序的,因此执行顺序可能会因构建而异。您可以通过声明一个任务对另一个任务的依赖关系来显式地告诉 Gradle 两个任务之间的顺序,例如 consumer.dependsOn producer
。
声明显式任务依赖 #
让我们看一个示例项目,其中包含一个常见模式。对于此项目,我们需要创建一个 zip 文件,其中包含来自 generator
任务的输出。generator
任务创建文件的方式并不重要——它生成包含递增数字的文件。
build.gradle #
apply plugin: 'base'
task generator() {
doLast {
def generatedFileDir = file("$buildDir/generated")
generatedFileDir.mkdirs()
for (int i=0; i<10; i++) {
new File(generatedFileDir, "${i}.txt").text = i
}
}
}
task zip(type: Zip) {
dependsOn generator
from "$buildDir/generated"
}
构建可以工作,但是构建脚本存在一些问题。generator
任务的输出目录在 zip
任务中重复,并且 zip
任务的依赖项使用 dependsOn
显式设置。Gradle 似乎每次都执行 generator
任务,但不会执行 zip
任务。现在是时候指出 Gradle 的最新检查与其他工具(如 Make)的不同之处了。Gradle 比较输入和输出的校验和,而不仅仅是文件的时间戳。即使 generator
任务每次都运行并覆盖其所有输出文件,但内容没有更改,并且 zip
任务不需要再次运行。zip
任务输入的校验和未更改。跳过最新的任务可以让 Gradle 避免不必要的工作并加快开发反馈循环。
声明任务输入和输出 #
现在,让我们了解为什么 generator
任务似乎每次都运行。如果我们查看 Gradle 的 info 级别日志输出,通过使用 --info
运行构建,我们将看到原因
Executing task ':generator' (up-to-date check took 0.0 secs) due to:
Task has not declared any outputs.
我们可以看到 Gradle 不知道该任务产生任何输出。默认情况下,如果任务没有任何输出,则必须将其视为过时。TaskOutputs 用于声明输出。任务输出可以是文件或目录。请注意下面 outputs
的用法
build.gradle #
task generator() {
def generatedFileDir = file("$buildDir/generated")
outputs.dir generatedFileDir
doLast {
generatedFileDir.mkdirs()
for (int i=0; i<10; i++) {
new File(generatedFileDir, "${i}.txt").text = i
}
}
}
如果我们再次运行构建两次,我们将看到 generator
任务在第一次运行后表示它是最新的。如果我们再次查看 --info
输出,我们可以确认这一点
Skipping task ':generator' as it is up-to-date (took 0.007 secs).
但是我们引入了一个新问题。如果我们增加生成的文件数(例如,从 10 个增加到 20 个),generator
任务不会重新运行。我们可以通过在每次需要更改该参数时执行清理构建来解决此问题,但是这种解决方法容易出错。
我们可以告诉 Gradle 什么会影响 generator
任务并要求其重新执行。我们可以使用 TaskInputs 来声明某些属性作为任务的输入以及输入文件。如果这些输入中的任何一个发生更改,Gradle 将知道执行该任务。请注意下面 inputs
的用法
build.gradle #
task generator() {
def fileCount = 10
inputs.property "fileCount", fileCount
def generatedFileDir = file("$buildDir/generated")
outputs.dir generatedFileDir
doLast {
generatedFileDir.mkdirs()
for (int i=0; i<fileCount; i++) {
new File(generatedFileDir, "${i}.txt").text = i
}
}
}
我们可以通过在更改 fileCount
属性的值后检查 --info
输出来验证这一点
Executing task ':generator' (up-to-date check took 0.007 secs) due to:
Value of input property 'fileCount' has changed for task ':generator'
推断任务依赖 #
到目前为止,我们只处理了 generator
任务,但是我们没有减少构建脚本中的任何重复。CopySpec#from 使用 Project#files 评估参数的方式,尝试删除任务依赖,从而依赖于这种方式。Gradle 可以自动为我们添加任务依赖。这也将 generator
任务的输出添加为 zip
任务的输入。
build.gradle #
task zip(type: Zip) {
from generator
}
当任务之间存在强烈的生产者-消费者关系时,推断的任务依赖可能比显式任务依赖更容易维护。当您只需要另一个任务的某些输出时,显式任务依赖通常会更简洁。如果更容易理解,则同时使用显式任务依赖和推断依赖都没有错。
使用自定义任务简化 #
我们将像 generator
这样的任务称为临时任务。它们没有明确定义的属性,也没有要执行的预定义操作。使用临时任务执行简单操作是可以的,但更好的做法是将临时任务移至自定义任务类中。自定义任务使您可以删除大量样板代码并标准化构建中的常见操作。
Gradle 使添加新的任务类型变得非常容易。您可以直接在构建文件中开始尝试自定义任务类型。当使用诸如 @OutputDirectory
之类的注释时,Gradle 将在您的任务执行之前创建输出目录,因此您不必担心自己创建目录。其他注释,例如 @Input
和 @InputFiles
,具有与手动配置任务的 TaskInputs
相同的效果。
尝试创建一个名为 Generate
的自定义任务类,该类生成与上面的 generator
任务相同的输出。您的构建文件应如下所示
build.gradle #
task generator(type: Generate) {
fileCount = 20
}
task zip(type: Zip) {
from generator
}
这是我们的解决方案
build.gradle #
class Generate extends DefaultTask {
@Input
int fileCount = 10
@OutputDirectory
File generatedFileDir = project.file("${project.buildDir}/generated")
@TaskAction
void perform() {
for (int i=0; i<fileCount; i++) {
new File(generatedFileDir, "${i}.txt").text = i
}
}
}
请注意,我们不再需要手动创建输出目录。 generatedFileDir
上的注释为我们处理了此事。fileCount
上的注释告诉 Gradle,该属性应被视为输入,就像我们之前使用 inputs.property
一样。最后,perform()
上的注释定义了 Generate
任务的操作。
关于增量构建的最终说明 #
在开发您自己的构建脚本、插件和自定义任务时,声明任务输入和输出是一项重要的技术,需要放在您的工具箱中。所有核心 Gradle 任务都有效地使用了此技术。如果您想了解在更低的级别上使任务增量化的其他方法,请查看正在孵化的增量任务支持。增量任务提供了一种细粒度的方式,用于在任务需要执行时仅构建已更改的内容。