自 Gradle 5.1 起,我们建议在创建任务时使用配置回避 API。

writing tasks 4

任务配置回避 API

如果任务在构建中不会被使用,配置回避 API 可以避免对其进行配置,这可以显著缩短总配置时间。

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

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

当任务被注册时,构建会知道它的存在。可以配置该任务,并且其引用可以在构建中传递,但任务对象本身尚未创建,其操作也尚未执行。注册的任务将保持此状态,直到构建中的某个部分需要实例化该任务对象。如果任务对象从未被需要,则任务将保持注册状态,从而避免创建和配置任务的开销。

在 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 那样工作。

使用配置回避进行任务排序

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

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

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

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

  • 通过 Task.dependsOn(…​)Task.finalizedBy(…​) 建立的关系是强关系,它们会强制执行引用的任务,即使这些任务原本不会被创建。

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

  • 如果一个任务执行,所有与其强关联的任务必须在配置阶段被创建和配置,因为它们可能具有其他 dependsOnfinalizedBy 关系。这将递归发生,直到任务图包含所有强关系。

迁移指南

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

迁移指南

  1. 在迁移过程中使用 help 任务作为基准。
    help 任务是衡量迁移过程的完美候选。在一个仅使用配置回避 API 的构建中,构建扫描显示在配置阶段没有任务被创建,只有被执行的任务才会被创建。

  2. 仅在配置 action 内部修改当前任务。
    由于任务配置 action 现在可以立即运行、稍后运行或从不运行,修改当前任务之外的任何内容都可能导致构建中出现不确定行为。请考虑以下代码

    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 被实例化时才会发生。为了避免这类问题,您必须仅修改与配置 action 关联的任务。其他任务应在其自己的配置 action 中修改

    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,可能会被禁止。
    例如,在使用新 API 注册的任务配置时,不能调用 Project.afterEvaluate()。由于 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。

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

迁移故障排除

  • 哪些任务正在被实例化? 使用 Build Scan 按照以下步骤进行故障排除

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

    2. 导航到配置性能标签页

      taskConfigurationAvoidance navigate to performance
    3. 将呈现所有必需的信息

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

        • Created immediately 表示使用急切任务 API 创建的任务。

        • Created during configuration 表示使用配置回避 API 创建的任务,但通过显式(通过 TaskProvider#get())或隐式(使用急切任务查询 API)方式被实例化。

        • Created immediatelyCreated during configuration 这两个数字都被视为应尽可能最小化的“不良”数字。

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

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

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

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

      3. 重点关注某个脚本、插件或生命周期回调,将显示被创建任务的详细 breakdown。

迁移陷阱

  • 警惕隐藏的急切任务实例化。 有许多方式可以急切地配置任务。
    例如,使用任务名称和 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
      }
    • 将 consumer 任务迁移到新的 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()

避免调用此方法。未来行为可能发生变化。