如何優雅地擴展 AGP 插件

語言: CN / TW / HK

theme: smartblue highlight: a11y-dark


前言

我們在項目中常常需要擴展AGP插件,比如重命名APK,校驗Manifest文件,對項目中的圖片做統一壓縮等操作。
總得來説,我們需要對AGP插件編譯過程中的中間產物(即Artifact)做一些操作,但是老版的AGP並沒有提供相關的API,導致我們需要去查看相關的代碼的具體實現,並通過反射等手段獲取對應產物。

本文主要介紹舊版AGP插件在擴展方面存在的問題,並介紹新版Variant APIArtifact API的使用,通過簡潔優雅的方式擴展AGP插件

目前存在的問題

內部實現沒有與API分離

舊版 AGP 沒有與內部實現明確分開的官方 API,導致用户使用時常常依賴了內部的具體實現,在開發AGP插件時,我們常常直接依賴於com.android.tools.build:gradle:x.x.x

為此從 7.0 版本開始,AGP 將提供一組穩定的官方 API,即com.android.tools.build:gradle-api:x.x.x。理論上説,在編寫插件時,現在建議僅依賴 gradle-api 工件,以便僅使用公開的接口和類。

開發插件時依賴Task的具體實現

由於舊版AGP沒有提供對應的API獲取相應產物,我們一般需要先了解是哪個Task處理了相關的資源,然後查看該Task的具體實現,通過相關API或者反射手段獲取對應的產物。

比如我們要開發一個校驗Manifest文件的插件,在舊版AGP中需要通過以下方式處理:

```kotlin def processManifestTask: ManifestProcessorTask = project.tasks.getByName("process${variant.name.capitalize()}Manifest") processManifestTask.doLast { logger.warn("main manifest: " + taskCompat.mainManifest) logger.warn("manifestOutputDirectory: " + taskCompat.manifestOutputDirectory) //...

}

} ```

這種寫法主要有幾個問題:
1. 成本較高,開發時需要了解相應Task的具體實現
2. AGP升級時相應Task的實現常常會發生變化,因此每次升級都需要做一系列的適配工作

總得來説,系統會將 AGP 創建的任務視為實現細節,不會作為公共 API 公開。您必須避免嘗試獲取 Task 對象的實例或猜測 Task 名稱,以及直接向這些 Task 對象添加回調或依賴項。

Variant API介紹

關於Variant API,我們首先了解一下什麼是Variant?
Variant官網翻譯為變體,看起來不太好理解。其實Variant就是buildTypeFlavor的組合,如下所示,2個buildType與2個Flavor可以組合出4個Variant

Variant APIAGP 中的擴展機制,可讓您操縱build.gradle中的各種配置。您還可以通過 Variant API 訪問構建期間創建的中間產物和最終產物,例如類文件、合併後的ManifestAPK/AAB 文件。

Android構建流程和擴展點

AGP 主要通過以下幾步來創建和執行其 Task 實例

  • DSL 解析:發生在系統評估 build 腳本時,以及創建和設置 android 代碼塊中的 Android DSL 對象的各種屬性時。後面幾部分中介紹的 Variant API 回調也是在此階段註冊的。
  • finalizeDsl():您可以通過此回調在 DSL 對象因組件(變體)創建而被鎖定之前對其進行更改。VariantBuilder 對象是基於 DSL 對象中包含的數據創建的。
  • DSL 鎖定:DSL 現已被鎖定,無法再進行更改。
  • beforeVariants():此回調可通過 VariantBuilder 影響系統會創建哪些組件以及所創建組件的部分屬性。它還支持對 build 流程和生成的工件進行修改。
  • 變體創建:將要創建的組件和工件列表現已最後確定,無法更改。
  • onVariants():在此回調中,您可以訪問已創建的 Variant 對象,您還可以為它們包含的 Property值設置值或Provider,以進行延遲計算。
  • 變體鎖定:變體對象現已被鎖定,無法再進行更改。
  • 任務已創建:使用 Variant 對象及其 Property 值創建執行 build 所必需的 Task 實例。

總得來説,為我們提供了3個回調方法,它們各自的使用場景如上圖所示,相比老版本的Variant API,新版本的生命週期劃分的更加清晰細緻

Artifact API介紹

Artifact即產物,Artifact API即我們獲取中間產物或者最終產物的API,通過Artifact API我們可以對中間產物進行增刪改查操作而不用關心具體的實現

每個Artifact類都可以實現以下任一接口,以表明自己支持哪些操作:
- Transformable:允許將 Artifact 作為輸入,供在該接口上執行任意轉換並輸出新版 ArtifactTask 使用。 - Appendable:僅適用於作為 Artifact.Multiple 子類的工件。它意味着可以向 Artifact 附加內容,也就是説,自定義 Task 可以創建此 Artifact 類型的新實例,這些實例將添加到現有列表中。 - Replaceable:僅適用於作為 Artifact.Single 子類的工件。可替換的 Artifact 可以被作為 Task 輸出生成的全新實例替換。

除了支持上述三種工件修改操作之外,每個工件還支持 get()(或 getAll())操作;該操作會返回 Provider 以及該產物的最終版本(在對產物的所有操作均完成之後)。

Get操作

Get操作可以獲得產物的最終版本(在對產物的所有操作完成之後),比如你想要檢查merged manifest文件,可以通過以下方法實現

```kotlin abstract class VerifyManifestTask: DefaultTask() { @get:InputFile abstract val mergedManifest: RegularFileProperty

@TaskAction
fun taskAction() {
    val mergedManifestFile = mergedManifest.get().asFile 
    // ... verify manifest file content ...
}

}

androidComponents { onVariants { // 註冊Task project.tasks.register("${name}VerifyManifest") { // 獲取merged_manifest並給Task設值 mergedManifest.set(artifacts.get(ArtifactType.MERGED_MANIFEST)) } } } ```

如上所示,通過以上方式,不需要知道依賴於哪個Task,也不需要了解Task的具體實現,就可以輕鬆獲取MERGED_MANIFEST的內容

Transformation操作

Transformation即變換操作,它可以把原有產物作為輸入值,進行變換後將結果作為輸出值,並傳遞給下一個變換

下面我們來看一個將ManifestVersionCode替換為git head的變換示例:

```kotlin // 獲取git head的Task abstract class GitVersionTask: DefaultTask() {

@get:OutputFile
abstract val gitVersionOutputFile: RegularFileProperty

@TaskAction
fun taskAction() {
    val process = ProcessBuilder("git", "rev-parse --short HEAD").start()
    val error = process.errorStream.readBytes().decodeToString()
    if (error.isNotBlank()) {
        throw RuntimeException("Git error : ${'$'}error")
    }
    var gitVersion = process.inputStream.readBytes().decodeToString()
    gitVersionOutputFile.get().asFile.writeText(gitVersion)
}

}

// 修改Manifest的Task abstract class ManifestTransformerTask: DefaultTask() {

@get:InputFile
abstract val gitInfoFile: RegularFileProperty

@get:InputFile
abstract val mergedManifest: RegularFileProperty

@get:OutputFile
abstract val updatedManifest: RegularFileProperty

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

}

// 註冊Task androidComponents { onVariants { // 創建git Version Task Provider val gitVersion = tasks.register("gitVersion") { gitVersionOutputFile.set( File(project.buildDir, "intermediates/git/output")) }

    // 創建修改Manifest的Task
    val manifestUpdater = tasks.register<ManifestTransformerTask>("${name}ManifestUpdater") {       
            // 把GitVersionTask的結果設置給gitInfoFile                  
            gitInfoFile.set(
                gitVersion.flatMap(GitVersionTask::gitVersionOutputFile)
            )
    }

    // manifestUpdater Task 與 AGP 進行連接
    artifacts.use(manifestUpdater)
        .wiredWithFiles(
                ManifestTransformerTask::mergedManifest,
                ManifestTransformerTask::updatedManifest)
       .toTransform(ArtifactType.MERGED_MANIFEST)  
}

} `` 通過以上操作,就可以把git head的值替換Manifest中的內容,可以注意到manifestUpdater依賴於gitVersion,但我們卻沒有寫dependsOn相關的邏輯。 這是因為它們都是TaskProvider類型,TaskPrOVIDER還攜帶有任務依賴項信息。當您通過flatmap一個 Task的輸出來創建Provider時,該Task會成為相應Provider的隱式依賴項,無論Provider的值在何時進行解析(例如當另一個Task需要它時),系統都要會創建並運行該Task`。

同理,我們也自動隱式依賴了process${variant.name.capitalize()}Manifest這個Task

Append操作

Append 僅與使用 MultipleArtifact 修飾的產物類型相關。 由於此類類型表示為 DirectoryRegularFile 的列表,因此任務可以聲明將附加到列表的輸出。

```kotlin // 聲明Task abstract class SomeProducer: DefaultTask() {

@get:OutputFile
abstract val output: RegularFileProperty

@TaskAction
fun taskAction() {
    val outputFile = output.get().asFile 
    // … write file content …
}

}

// 註冊Task androidComponents { onVariants { val someProducer = project.tasks.register("${name}SomeProducer") { // ... configure your task as needed ... } artifacts.use(someProducer) .wiredWith(SomeProducerTask::output) .toAppendTo(ArtifactType.MANY_ARTIFACT) } } ```

Creation操作

此操作用一個新的 Artifact 替換當前的Artifact,丟棄所有以前的Artifact。這是一個“輸出”操作,其中任務聲明自己是產物的唯一提供者。如果有多個任務將自己聲明為產物提供者,則最後一個將獲勝。

例如,自定義任務可能不使用內置清單合併,而是通過代碼寫入一個新的。

```kotlin // 聲明Task abstract class ManifestFileProducer: DefaultTask() {

@get:OutputFile
abstract val outputManifest: RegularFileProperty

@TaskAction
fun taskAction() {
    val mergedManifestFile = outputManifest.get().asFile 
    // ... write manifest file content ...
}

}

// 註冊Task androidComponents { onVariants { val manifestProducer = project.tasks.register("${name}ManifestProducer") { //… configure your task as needed ... } artifacts.use(manifestProducer) .wiredWith(ManifestProducerTask::outputManifest) .toCreate(ArtifactType.MERGED_MANIFEST) } } ```

Artifact API的問題

Artifact API目前的問題就在於支持的產物類型還有限,只有以下幾種

SingleArtifact.APK SingleArtifact.MERGED_MANIFEST SingleArtifact.OBFUSCATION_MAPPING_FILE SingleArtifact.BUNDLE SingleArtifact.AAR SingleArtifact.PUBLIC_ANDROID_RESOURCES_LIST SingleArtifact.METADATA_LIBRARY_DEPENDENCIES_REPORT MultipleArtifact.MULTIDEX_KEEP_PROGUARD MultipleArtifact.ALL_CLASSES_DIRS MultipleArtifact.ALL_CLASSES_JARS MultipleArtifact.ASSETS

還有很多常用的中間產物如MERGED_RESMERGED_NATIVE_LIBS等都還不支持,因此在現階段還是不可避免的使用老版本的API來獲取這些資源

總結

新版Variant API通過專注於產物而不是Task,自定義插件或構建腳本可以安全地擴展 AGP 插件,而不受構建流程更改或Task實現等內部細節的支配。開發者不需要知道要修改的產物依賴於哪個Task,也不需要知道Task的具體實現,可以有效降低我們開發與升級Gradle插件的成本。

雖然目前新版Variant API支持的獲取的中間產物類型還有限,但這也應該可以確定是AGP插件擴展將來的方向了。在AGP8.0中,舊版 Variant API將被廢棄,而在AGP9.0中,舊版Vaiant API將被刪除,並將移除對私有內部 AGP 類的訪問權限,因此現在應該是時候瞭解一下新版Variant API的使用了~

示例代碼

本文所有代碼可見:http://github.com/android/gradle-recipes/

參考資料

New APIs in the Android Gradle Plugin
社區説|擴展 Android 構建流程 - 基於新版 Variant/Artifact APIs
擴展 Android Gradle 插件