如何優雅地擴充套件 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的使用了~

示例程式碼

本文所有程式碼可見:https://github.com/android/gradle-recipes/

參考資料

New APIs in the Android Gradle Plugin
社群說|擴充套件 Android 構建流程 - 基於新版 Variant/Artifact APIs
擴充套件 Android Gradle 外掛