介绍增量构建支持

目录

  • 声明任务输入和输出
  • 推断任务依赖
  • 使用自定义任务简化
  • 关于增量构建的最终说明
  • 任务输入、输出和依赖 #

    内置任务,例如 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 任务都有效地使用了此技术。如果您想了解在更低的级别上使任务增量化的其他方法,请查看正在孵化的增量任务支持。增量任务提供了一种细粒度的方式,用于在任务需要执行时仅构建已更改的内容。

    讨论