Gradle 與 AGP 構建 API: 進一步完善您的外掛!

Android開發者發表於2022-01-06

歡迎閱讀 MAD Skills 系列 之 Gradle 與 AGP 構建 API 的第三篇文章。在上一篇文章《Gradle 與 AGP 構建 API: 如何編寫外掛》中,您學習瞭如何編寫您自己的外掛,以及如何使用 Variants API

如果您更喜歡通過視訊瞭解此內容,請 點選此處 檢視。

在本文中,您將會學習 Gradle 的 Task、Provider、Property 以及使用 Task 進行輸入與輸出。同時您也將進一步完善您的外掛,並學習如何使用新的 Artifact API 訪問各種構建產物。

Property

假設我想要建立一個外掛,該外掛可以使用 Git 版本自動更新應用清單檔案中指定的版本號。為了達到這一目標,我需要為構建新增兩個 Task。第一個 Task 會獲取 Git 版本,而第二個 Task 將會使用該 Git 版本來更新清單檔案。

讓我們從建立名為 GitVersionTask 的新任務開始。GitVersionTask 需要繼承 DefaultTask,同時實現帶有註解的 taskAction 函式。下面是查詢 Git 樹頂端資訊的程式碼。

abstract class GitVersionTask: DefaultTask() {
   @TaskAction
   fun taskAction(){
       // 這裡是獲取樹版本頂端的程式碼
       val process = ProcessBuilder(
           "git",
           "rev-parse --short HEAD"
       ).start()
       val error = process.errorStream.readBytes().toString()
       if (error.isNotBlank()) {
           System.err.println("Git error : $error")
       }
       var gitVersion = process.inputStream.readBytes().toString()
       //...
   }
}

我不能直接快取版本資訊,因為我想將它儲存在一箇中間檔案中,從而讓其他 Task 也可以讀取和使用這個值。為此,我需要使用 RegularFileProperty。Property 可以用於 Task 的輸入與輸出。在本例中,Property 將會作為呈現 Task 輸出的容器。我建立了一個 RegularFileProperty,並使用 @get:OutputFile 對其進行註解。OutputFile 是附加至 getter 函式的標記註解。此註解會將 Property 標記為該 Task 的輸出檔案。

@get:OutputFile
abstract val gitVersionOutputFile: RegularFileProperty

現在,我已經宣告瞭 Task 的輸出,讓我們回到 taskAction() 函式,我會在這裡訪問檔案並寫入我想要儲存的文字。本例中,我會儲存 Git 版本,也就是 Task 的輸出。為了簡化示例,我將查詢 Git 版本的程式碼替換為了硬編碼字串。

abstract class GitVersionTask: DefaultTask() {
   @get:OutputFile
   abstract val gitVersionOutputFile: RegularFileProperty
   @TaskAction
   fun taskAction() {
       gitVersionOutputFile.get().asFile.writeText("1234")
   }
}

現在,Task 已經準備就緒,讓我們在外掛程式碼中對其進行註冊。首先,我會建立一個名為 ExamplePlugin 的新外掛類,並在其中實現 Plugin。如果您不熟悉在 buildSrc 資料夾中建立外掛的流程,可以回顧本系列的前兩篇文章:《Gradle 與 AGP 構建 API: 配置您的構建檔案》、《Gradle 與 AGP 構建 API: 如何編寫外掛》。

△ buildSrc 資料夾

△ buildSrc 資料夾

接下來我會註冊 GitVersionTask 並將檔案 Property 設定為輸出到 build 資料夾中的一箇中間檔案上。我同時還將 upToDateWhen 設定為 false,這樣此 Task 前一次執行的輸出就不會被複用。這也意味著由於該 Task 不會處於最新的狀態,因此每次構建時都會被執行。

override fun apply(project: Project) {
   project.tasks.register(
       "gitVersionProvider",
       GitVersionTask::class.java
   ) {
       it.gitVersionOutputFile.set(
           File(
               project.buildDir,  
               "intermediates/gitVersionProvider/output"
           )
       )
       it.outputs.upToDateWhen { false }
    }
}

在 Task 執行完畢後,我就可以檢查位於 build/intermediates 資料夾下的 output 檔案了。我只要驗證 Task 是否儲存了我所硬編碼的值即可。

接下來讓我們轉向第二個 Task,該 Task 會更新清單檔案中的版本資訊。我將它命名為 ManifestTransformTask,並使用兩個 RegularFileProperty 物件作為它的輸入值。

abstract class ManifestTransformerTask: DefaultTask() {
   @get:InputFile
   abstract val gitInfoFile: RegularFileProperty
   @get:InputFile
   abstract val mergedManifest: RegularFileProperty
}

我會用第一個 RegularFileProperty 讀取 GitVersionTask 生成的輸出檔案中的內容;用第二個 RegularFileProperty 讀取應用的清單檔案。然後我就可以用 gitInfoFile 檔案中 gitVersion 變數所儲存的版本號替換清單檔案中的版本號了。

@TaskAction
fun taskAction() {
   val gitVersion = gitInfoFile.get().asFile.readText()
   var manifest = mergedManifest.asFile.get().readText()
   manifest = manifest.replace(
       "android:versionCode=\"1\"",    
       "android:versionCode=\"${gitVersion}\""
   )
  
}

現在,我可以寫入更新後的清單檔案了。首先,我會為輸出建立另一個 RegularFileProperty,並使用 @get:OutputFile 對其進行註解。

@get:OutputFile
abstract val updatedManifest: RegularFileProperty
注意: 我本可以使用 VariantOutput 直接設定 versionCode,而無需重寫清單檔案。但是為了向您展示如何使用構建產物轉換,我會通過本示例的方式得到相同的效果。

讓我們回到外掛,並將一切聯絡起來。我首先獲得 AndroidComponentsExtension。我希望在 AGP 決定建立哪個變體後、在各種物件的值被鎖定而無法被修改之前執行這一新 Task。onVariants() 回撥會在 beforeVariants() 回撥後呼叫,後者可能會讓您想起 前一篇文章

val androidComponents = project.extensions.getByType(
   AndroidComponentsExtension::class.java
)
androidComponents.onVariants { variant ->
   //...
}

Provider

您可以使用 Provider 連線 Property 到其他需要執行耗時操作 (例如讀取檔案或網路等外部輸入) 的 Task。

我會從註冊 ManifestTransformerTask 開始。此 Task 依賴 gitVersionOutput 檔案,而該檔案是前一個 Task 的輸出。我將通過使用 Provider 來訪問這一 Property

val manifestUpdater: TaskProvider = project.tasks.register(
   variant.name + "ManifestUpdater",  
   ManifestTransformerTask::class.java
) {
   it.gitInfoFile.set(
       //...
   )
}

Provider 可以用於訪問指定型別的值,您可以直接使用 get() 函式,也可以使用操作符函式 (如 map()flatMap()) 將值轉換為新的 Provider。在我回顧 Property 介面時,發現其實現了 Property 介面。您可以將值惰性地設定給 Property,並在稍候惰性地使用 Provider 訪問這些值。

當我檢視 register() 的返回型別時,發現它返回了給定型別的 TaskProvider。我將其賦值給了一個新的 val

val gitVersionProvider = project.tasks.register(
   "gitVersionProvider",
   GitVersionTask::class.java
) {
   it.gitVersionOutputFile.set(
       File(
           project.buildDir,
           "intermediates/gitVersionProvider/output"
       )
    )
    it.outputs.upToDateWhen { false }
}

現在我們回過頭來設定 ManifestTransformerTask 的輸入。在我嘗試將來自 Provider 的值對映為輸入 Property 時,產生了一個錯誤。map() 的 lambda 引數接收某種型別 (如 T) 的值,該函式會產生另一個型別 (如 S) 的值。

△ 使用 map() 時造成的錯誤

△ 使用 map() 時造成的錯誤

然而,在本例中,set 函式需要 Provider 型別。我可以使用 flatMap() 函式,該函式也接收一個 T 型別的值,但會產生一個 S 型別的 Provider,而不是直接產生 S 型別的值。

it.gitInfoFile.set(
   gitVersionProvider.flatMap(
       GitVersionTask::gitVersionOutputFile
   )
)

轉換

接下來,我需要告訴變體的產物使用 manifestUpdater,同時將清單檔案作為輸入,將更新後的清單檔案作為輸出。最後,我呼叫 toTransform()) 函式轉換單個產物的型別。

variant.artifacts.use(manifestUpdater)
  .wiredWithFiles(
      ManifestTransformerTask::mergedManifest,
      ManifestTransformerTask::updatedManifest
  ).toTransform(SingleArtifact.MERGED_MANIFEST)

在執行此 Task 時,我可以看到應用清單檔案中的版本號被更新成了 gitVersion 檔案中的值。需要注意的是,我並沒有顯式地要求 GitProviderTask 執行。該任務之所以被執行,是因為其輸出是 ManifestTransformerTask 的輸入,而後者是我所請求執行的。

BuiltArtifactsLoader

讓我們新增另一個 Task,來了解如何訪問已被更新的清單檔案並驗證它是否被更新成功。我會建立一個名為 VerifyManifestTask 的新任務。為了讀取清單檔案,我需要訪問 APK 檔案,該檔案是構建 Task 的產物。為此,我需要將構建 APK 資料夾作為 Task 的輸入。

注意,這次我使用了 DirectoryProperty 而不是 FileProperty,因為 SingleArticfact.APK 物件可以表示構建之後存放 APK 檔案的目錄。

我還需要一個型別為 BuiltArtifactsLoader 的 Property 作為 Task 的第二個輸入,我會用它從後設資料檔案中載入 BuiltArtifacts 物件。後設資料檔案描述了 APK 目錄下的檔案資訊。若您的專案包含原生元件、多種語言等要素,那麼每次構建都可以產生數個 APK。BuiltArtifactsLoader 抽象了識別每個 APK 及其屬性 (如 ABI 和語言) 的過程。

@get:Internal
abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>

是時候實現 Task 了。首先我載入了 buildArtifacts,並保證其中只包含了一個 APK,接著將此 APK 作為 File 例項進行載入。

val builtArtifacts = builtArtifactsLoader.get().load(
   apkFolder.get()
)?: throw RuntimeException("Cannot load APKs")
if (builtArtifacts.elements.size != 1)
  throw RuntimeException("Expected one APK !")
val apk = File(builtArtifacts.elements.single().outputFile).toPath()

這時,我已經可以訪問 APK 中的清單檔案並驗證版本是否已經更新成功。為了保持示例的簡潔,我在這裡只會檢查 APK 是否存在。我還新增了一個 "在此處檢查清單檔案" 的提醒,並列印了成功的資訊。

println("Insert code to verify manifest file in ${apk}")
println("SUCCESS")

現在我們回到外掛的程式碼以註冊此 Task。在外掛程式碼中,我將此 Task 註冊為 "Verifier",並傳入 APK 資料夾和當前變體產物的 buildArtifactLoader 物件。

project.tasks.register(
   variant.name + "Verifier",
   VerifyManifestTask::class.java
) {
   it.apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
   it.builtArtifactsLoader.set(
       variant.artifacts.getBuiltArtifactsLoader()
   )
}

當我再次執行 Task 時,可以看到新的 Task 載入了 APK 並列印了成功資訊。注意,這次我依舊沒有顯式請求清單轉換的執行,但是因為 VerifierTask 請求了最終版本的清單產物,所以自動進行了轉換。

總結

我的 外掛 中包含三個 Task: 首先,外掛會檢查當前 Git 樹,並將版本儲存在一箇中間檔案中;隨後,外掛會惰性使用上一步的輸出,並使用一個 Provider 將版本號更新至當前的清單檔案;最後,外掛會使用另一個 Task 訪問構建產物,並檢查清單檔案是否正確更新。

以上就是全部內容!從 7.0 版開始,Android Gradle 外掛提供了官方的擴充套件點,以便您編寫自己的外掛。使用這些新 API,您可以控制構建輸入、讀取、修改甚至替換中間和最終產物。

如需瞭解更多內容,學習如何保持您構建的高效性,請查閱 官方文件gradle-recipes

歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!

相關文章