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

writing tasks 4

任务配置避免 API

配置避免 API 避免在构建中不使用任务时配置任务,这可以显着影响总配置时间。

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

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

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

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

指南

延迟任务创建

为了有效地避免任务配置,构建作者需要将 TaskContainer.create(java.lang.String) 的实例更改为 TaskContainer.register(java.lang.String)

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

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

延迟任务配置

DomainObjectCollection.all(org.gradle.api.Action)DomainObjectCollection.withType(java.lang.Class, org.gradle.api.Action) 这样的急切 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()。这将导致任务被创建和配置,但所有操作都应与使用急切 API 时一样工作。

避免配置时的任务排序

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

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

当需要建立任务关系(例如,dependsOnfinalizedBymustRunAftershouldRunAfter)时,可以在软关系和强关系之间进行区分。它们在配置阶段对任务创建的影响不同。

  • Task.mustRunAfter(…​)Task.shouldRunAfter(…​) 代表软关系,它们只能改变现有任务的顺序,但不能触发它们的创建。

  • Task.dependsOn(…​)Task.finalizedBy(…​) 代表强关系,它们强制执行引用任务,即使它们原本没有被创建。

  • 如果一个任务没有被执行,无论它是使用 Task.register(…​) 还是 Task.create(…​) 创建的,定义的关系都不会在配置时触发任务创建。

  • 如果一个任务执行,所有与其强关联的任务都必须在配置时被创建和配置,因为它们可能还有其他 dependsOnfinalizedBy 关系。这将以递归的方式进行,直到任务图包含所有强关系。

迁移指南

以下部分将介绍一些 一般指南,以便在迁移构建逻辑时遵循。我们还提供了一些推荐的 步骤,以及 故障排除常见陷阱

迁移指南

  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,但在这个例子中,它不会执行。这是因为 verificationTaskcheck 之间的依赖关系只有在 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 用于延迟配置 Project,因此将延迟配置与新 API 混合可能会导致难以诊断的错误,因为使用新 API 注册的任务并不总是被配置,但始终预期 afterEvaluate 块执行。

迁移步骤

迁移过程的第一步是遍历代码,手动将急切任务创建和配置迁移到使用配置避免 API。

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

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

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

完成这些更改后,您应该看到配置时急切创建的任务数量有所改善。

迁移故障排除

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

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

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

      taskConfigurationAvoidance navigate to performance
    3. 将显示所有必要的信息

      taskConfigurationAvoidance performance annotated
      1. 创建或未创建每个任务时存在的总任务数。

        • 立即创建 代表使用急切任务 API 创建的任务。

        • 配置期间创建 代表使用配置避免 API 创建的任务,但已明确实现(通过 TaskProvider#get())或隐式使用急切任务查询 API。

        • 立即创建配置期间创建 这两个数字都被认为是“不良”数字,应该尽可能地减少。

        • 任务执行期间创建 代表在创建任务图之后创建的任务。在此阶段创建的任何任务都不会作为图的一部分执行。理想情况下,此数字应为零。

        • 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

API 注意

返回 TaskProvider 而不是 Task

返回 TaskProvider 而不是 Task

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

返回 void,因此无法链接。

要避免的急切 API

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()

避免调用此方法。行为将来可能会改变。