從 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(…​) 可能不足以完全避免所有任務配置。您可能需要變更其他程式碼,這些程式碼依名稱或類型配置任務,請參閱下文。

延遲任務配置

急切 API,例如 DomainObjectCollection.all(org.gradle.api.Action)DomainObjectCollection.withType(java.lang.Class, org.gradle.api.Action) 將立即建立和配置任何已註冊的任務。若要延遲任務配置,您必須遷移到等效的配置避免 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),可以在軟關係和強關係之間進行區分。它們在配置階段對任務建立的影響不同

遷移指南

以下章節將介紹一些在遷移建置邏輯時要遵守的一般指南。我們還提供了一些建議的步驟,以及疑難排解常見陷阱

遷移指南

  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,則可能會被禁止。
    例如,當配置使用新 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。

在進行這些變更之後,您應該會看到在配置時急切建立的任務數量有所改善。

遷移疑難排解

  • 哪些任務正在被實現? 使用 建置掃描 透過以下步驟進行疑難排解

    1. 使用 --scan 標誌執行 Gradle 命令。

    2. 導航到配置效能標籤頁

      taskConfigurationAvoidance navigate to performance
    3. 將顯示所有需要的資訊

      taskConfigurationAvoidance performance annotated
      1. 在每個任務建立時或未建立時存在的總任務數。

        • 立即建立表示使用急切任務 API 建立的任務。

        • 在配置期間建立表示使用配置避免 API 建立的任務,但已明確實現(透過 TaskProvider#get())或使用急切任務查詢 API 隱含實現。

        • 立即建立在配置期間建立 數字都被認為是「壞」數字,應盡可能減少。

        • 在任務執行期間建立表示在任務圖建立之後建立的任務。此時建立的任何任務都不會作為圖形的一部分執行。理想情況下,此數字應為零。

        • 在任務圖計算期間建立表示在建置執行任務圖時建立的任務。理想情況下,此數字應等於執行的任務數。

        • 未建立表示在此建置會期中避免的任務。理想情況下,此數字應盡可能大。

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

避免呼叫此方法。行為可能會在未來變更。