从 Gradle 5.1 开始,我们建议在创建任务时使用配置避免 API。

写作任务4

任务配置回避API

如果任务不用于构建,配置避免 API 会避免配置任务,这可能会显着影响总配置时间。

例如,当运行一个compile任务(java应用了插件)时,其他不相关的任务(如cleantestjavadocs)将不会被执行。

为了避免创建和配置构建不需要的任务,我们可以注册该任务。

当任务被注册时,构建就知道它。它可以被配置,并且可以传递对它的引用,但是任务对象本身尚未创建,并且其操作尚未执行。已注册的任务将保持此状态,直到构建中的某些内容需要实例化的任务对象。如果永远不需要任务对象,则任务将保持注册状态,并且将避免创建和配置任务的成本。

在 Gradle 中,您可以使用TaskContainer.register(java.lang.String)注册任务。该方法不返回任务实例,而是register(…​)返回一个TaskProvider,它是对任务的引用,可以在许多可能使用普通任务对象的地方(即,创建任务依赖项时)使用该任务。

指南

推迟任务创建

旧版本的 Gradle 仅支持 API create(…​)。该create(…​)API 在调用时会急切地创建和配置任务,因此应避免这样做。

单独使用register(…​)可能不足以完全避免所有任务配置。您可能需要更改按名称或按类型配置任务的其他代码,请参见下文。

推迟任务配置

像DomainObjectCollection.all(org.gradle.api.Action)DomainObjectCollection.withType(java.lang.Class, org.gradle.api.Action)这样的 Eager API将立即创建和配置任何已注册的任务。要推迟任务配置,您必须迁移到等效的配置避免 API。请参阅下表以确定最佳替代方案。

引用已注册的任务

您可以通过TaskProvider对象处理已注册的任务,而不是引用任务对象。可以通过多种方式获取 TaskProvider,包括TaskContainer.register ( java.lang.String)TaskCollection.named(java.lang.String)方法。

调用Provider.get()或使用TaskCollection.getByName(java.lang.String)按名称查找任务将导致创建和配置任务。

像Task.dependsOn(java.lang.Object...​)ConfigurableFileCollection.builtBy(java.lang.Object...)这样的方法与TaskProvider 的工作方式与Task相同,因此您无需打开Provider显式依赖项即可继续工作。

您必须使用与按名称配置任务等效的配置避免。请参阅下表以确定最佳替代方案。

引用任务实例

如果您需要访问 Task 实例,可以使用TaskCollection.named(java.lang.String)Provider.get()。这将导致任务被创建和配置,但一切都应该像使用 eager API 一样工作。

避免配置的任务排序

调用排序方法本身不会导致任务创建。所有这些方法所做的就是声明关系。

这些关系的存在可能会间接导致构建过程后期阶段的任务创建。

当需要建立任务关系时(即,,,,,dependsOn),可以区分软关系和强关系。它们对配置阶段任务创建的影响不同:finalizedBymustRunAftershouldRunAfter

迁移指南

以下部分将介绍迁移构建逻辑时要遵循的一些一般准则。我们还提供了一些建议遵循的步骤以及故障排除常见陷阱

迁移指南

  1. 在迁移过程中使用help任务作为基准。
    help任务是对迁移过程进行基准测试的完美候选者。在仅使用配置避免 API 的构建中,构建扫描显示在配置期间没有创建任何任务,并且仅创建执行的任务。

  2. 仅在配置操作中改变当前任务。
    由于任务配置操作现在可以立即运行、稍后运行或从不运行,因此改变当前任务以外的任何内容都可能会导致构建中的不确定行为。考虑以下代码:

    val check by tasks.registering
    tasks.register("verificationTask") {
        // Configure verificationTask
    
        // Run verificationTask when someone runs check
        check.get().dependsOn(this)
    }
    def check = tasks.register("check")
    tasks.register("verificationTask") { verificationTask ->
        // Configure verificationTask
    
        // Run verificationTask when someone runs check
        check.get().dependsOn verificationTask
    }

    执行gradle check任务应该执行verificationTask,但在这个例子中,它不会。这是因为verificationTask和之间的依赖关系只有在实现check时才会发生。verificationTask为了避免此类问题,您必须仅修改与配置操作关联的任务。其他任务应在其自己的配置操作中进行修改:

    val check by tasks.registering
    val verificationTask by tasks.registering {
        // Configure verificationTask
    }
    check {
        dependsOn(verificationTask)
    }
    def check = tasks.register("check")
    def verificationTask = tasks.register("verificationTask") {
        // Configure verificationTask
    }
    check.configure {
        dependsOn verificationTask
    }

    将来,Gradle 会将这种反模式视为错误并产生异常。

  3. 更喜欢小的增量变化。
    较小的更改更容易进行健全性检查。如果您破坏了构建逻辑,则分析自上次成功验证以来的变更日志会更容易。

  4. 确保制定良好的计划来验证构建逻辑。
    通常,一个简单的build任务调用就可以验证您的构建逻辑。但是,某些构建可能需要额外的验证 - 了解构建的行为并确保您有一个良好的验证计划。

  5. 与手动测试相比,更喜欢自动测试。
    使用 TestKit 为构建逻辑编写集成测试是一种很好的做法。

  6. 避免通过名称引用任务。
    通常,通过名称引用任务是一种脆弱的模式,应该避免。尽管任务名称在 上可用TaskProvider,但应尽量使用强类型模型中的引用。

  7. 尽可能使用新的任务API。
    急切地实现某些任务可能会导致其他任务的级联实现。使用TaskProvider有助于创建一种间接的方式来防止传递实现。

  8. 如果您尝试从新 API 的配置块访问某些 API,它们可能会被禁止。
    例如,Project.afterEvaluate()在配置使用新 API 注册的任务时无法调用。由于afterEvaluate用于延迟配置 a Project,因此将延迟配置与新 API 混合可能会导致难以诊断的错误,因为使用新 API 注册的任务并不总是被配置,但afterEvaluate可能总是期望执行一个块。

迁移步骤

迁移过程的第一部分是检查代码并手动迁移急切任务创建和配置以使用配置避免 API。

  1. 迁移影响所有任务 ( tasks.all {}) 或按类型 ( tasks.withType(…​) {}) 划分的子集的任务配置。
    这将导致您的构建急切地创建更少的由插件注册的任务。

  2. 迁移按名称配置的任务。
    这将导致您的构建急切地创建更少的由插件注册的任务。例如,使用的逻辑TaskContainer#getByName(String, Closure)应转换为TaskContainer#named(String, Action).这还包括通过 DSL 块进行任务配置

  3. 将任务创建迁移到register(…​).
    此时,您应该将任何任务创建(使用create(…​)或类似)更改为使用寄存器。

进行这些更改后,您应该会看到在配置时急切创建的任务数量有所增加。

迁移故障排除

  • 正在实现哪些任务?按照以下步骤使用构建扫描进行故障排除:

    1. 使用该标志执行 Gradle 命令--scan

    2. 导航到配置性能选项卡:

      taskConfigurationAvoidance 导航至性能
    3. 将提供所需的所有信息:

      taskConfigurationAvoidance 性能注释
      1. 每个任务创建或未创建时存在的总任务数。

        • Created immediately表示使用 eager task API 创建的任务。

        • Created during configurationTaskProvider#get()表示使用配置避免 API 创建的任务,但显式(通过)或隐式使用 eager 任务查询 API实现。

        • Created immediately和数字都Created during configuration被认为是“坏”数字,应尽可能减少。

        • Created during task execution表示创建任务图创建的任务。此时创建的任何任务都不会作为图表的一部分执行。理想情况下,这个数字应该为零。

        • Created during task graph calculation表示构建执行任务图时创建的任务。理想情况下,该数字等于执行的任务数。

        • Not created表示在此构建会话中避免的任务。理想情况下,这个数字尽可能大。

      2. 下一节将帮助回答任务在哪里实现的问题。对于每个脚本、插件或生命周期回调,最后一列表示立即或在配置期间创建的任务。理想情况下,此列应为空。

      3. 重点关注脚本、插件或生命周期回调将显示已创建任务的详细信息。

迁移陷阱

  • 当心隐藏的急切任务实现。 可以通过多种方式快速配置任务。
    例如,使用任务名称和 DSL 块配置任务将导致立即创建任务(当使用 Groovy DSL 时):

    // Given a task lazily created with
    tasks.register("someTask")
    
    // Some time later, the task is configured using a DSL block
    someTask {
        // This causes the task to be created and this configuration to be executed immediately
    }

    相反,使用该named()方法来获取对任务的引用并对其进行配置:

    tasks.named("someTask") {
        // ...
        // Beware of the pitfalls here
    }

    同样,Gradle 具有语法糖,允许通过名称引用任务,而无需显式查询方法。这也可能导致立即创建任务:

    tasks.register("someTask")
    
    // Sometime later, an eager task is configured like
    task anEagerTask {
        // The following will cause "someTask" to be looked up and immediately created
        dependsOn someTask
    }

    有几种方法可以避免这种过早创建:

    • 使用TaskProvider变量。 当在同一构建脚本中多次引用任务时很有用。

      val someTask by tasks.registering
      
      task("anEagerTask") {
          dependsOn(someTask)
      }
      def someTask = tasks.register("someTask")
      
      task anEagerTask {
          dependsOn someTask
      }
    • 将消费者任务迁移到新的 API。

      tasks.register("someTask")
      
      tasks.register("anEagerTask") {
          dependsOn someTask
      }
    • 懒洋洋地查找任务。 当任务不是由同一插件创建时很有用。

      tasks.register("someTask")
      
      task("anEagerTask") {
          dependsOn(tasks.named("someTask"))
      }
      tasks.register("someTask")
      
      task anEagerTask {
          dependsOn tasks.named("someTask")
      }

使用惰性 API

应用程序编程接口 笔记

返回 aTaskProvider而不是 a Task

返回 aTaskProvider而不是 a Task

可以使用了。如果是链接的withType().getByName(),请TaskCollection.named()改为使用。

返回void,因此它不能被链接。

需要避免的急切 API

应用程序编程接口 笔记

task myTask(type: MyTask) {}

不要使用速记符号。代替使用register()

代替使用register()

不使用。

不使用。

避免这样称呼。该行为将来可能会改变。

代替使用named()

代替使用named()

代替使用DomainObjectCollection.configureEach()

如果您根据名称进行匹配,请使用named()它来代替,这会很懒。 matching()需要创建所有任务,因此请尝试通过限制任务类型来限制影响,例如withType().matching().

代替使用named()

代替使用withType().configureEach()

代替使用configureEach()

代替使用configureEach()

避免调用此方法。matching()并且configureEach()在大多数情况下更合适。

不使用。named()是最接近的等效项,但如果任务不存在,则会失败。

iterator()或对Task集合的隐式迭代

避免这样做,因为它需要创建和配置所有任务。

remove()

避免这样称呼。该行为将来可能会改变。