引入增量构建支持
任务输入、输出和依赖项 #
像JavaCompile这样的内置任务会声明一组输入(Java源文件)和一组输出(类文件)。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
任务似乎每次都会运行。如果我们通过运行带有--info
的构建来查看Gradle的信息级别日志输出,我们将看到原因。
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任务都有效地使用了这一点。如果您想了解在更低级别上使任务增量化的其他方法,请查看正在孵化中的增量任务支持。增量任务提供了一种细粒度的方式,在任务需要执行时仅构建已更改的部分。