介绍增量构建支持
任务输入、输出和依赖项
内置任务,例如 JavaCompile 声明一组输入(Java 源文件)和一组输出(类文件)。Gradle 使用此信息来确定任务是否是最新的,是否需要执行任何操作。如果输入或输出都没有改变,Gradle 可以跳过该任务。总的来说,我们将这种行为称为 Gradle 的增量构建支持。
要利用增量构建支持,您需要向 Gradle 提供有关任务输入和输出的信息。可以将任务配置为仅具有输出。在执行任务之前,Gradle 会检查输出,如果输出没有改变,则会跳过任务的执行。在实际构建中,任务通常也具有输入,包括源文件、资源和属性。 Gradle 会检查输入和输出是否都没有改变,然后再执行任务.
通常,一个任务的输出会作为另一个任务的输入。正确地设置这些任务之间的顺序非常重要,否则任务将以错误的顺序运行或根本无法运行。Gradle 不依赖于构建脚本中定义的任务顺序。新任务是无序的,因此执行顺序可能会在每次构建之间发生变化。您可以通过在两个任务之间声明依赖关系来明确地告诉 Gradle 它们之间的顺序,例如 consumer.dependsOn producer
。
声明显式任务依赖关系
让我们看一个包含常见模式的示例项目。对于这个项目,我们需要创建一个包含 generator
任务输出的 zip 文件。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
运行构建),我们将看到原因
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 任务 都非常有效地利用了这一点。如果您想了解在更低级别使您的任务增量的其他方法,请查看孵化中的 增量任务 支持。增量任务提供了一种细粒度的方式,仅在任务需要执行时构建已更改的部分。