如何優雅地擴展 AGP 插件
theme: smartblue highlight: a11y-dark
前言
我們在項目中常常需要擴展AGP
插件,比如重命名APK
,校驗Manifest
文件,對項目中的圖片做統一壓縮等操作。
總得來説,我們需要對AGP
插件編譯過程中的中間產物(即Artifact
)做一些操作,但是老版的AGP
並沒有提供相關的API
,導致我們需要去查看相關的代碼的具體實現,並通過反射等手段獲取對應產物。
本文主要介紹舊版AGP
插件在擴展方面存在的問題,並介紹新版Variant API
與Artifact 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
就是buildType
與Flavor
的組合,如下所示,2個buildType
與2個Flavor
可以組合出4個Variant
Variant API
是 AGP
中的擴展機制,可讓您操縱build.gradle
中的各種配置。您還可以通過 Variant API
訪問構建期間創建的中間產物和最終產物,例如類文件、合併後的Manifest
或 APK/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
作為輸入,供在該接口上執行任意轉換並輸出新版 Artifact
的 Task
使用。
- 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
如上所示,通過以上方式,不需要知道依賴於哪個Task
,也不需要了解Task
的具體實現,就可以輕鬆獲取MERGED_MANIFEST
的內容
Transformation
操作
Transformation
即變換操作,它可以把原有產物作為輸入值,進行變換後將結果作為輸出值,並傳遞給下一個變換
下面我們來看一個將Manifest
的VersionCode
替換為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
// 創建修改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
修飾的產物類型相關。 由於此類類型表示為 Directory
或 RegularFile
的列表,因此任務可以聲明將附加到列表的輸出。
```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
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
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_RES
,MERGED_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 插件
- kotlin-android-extensions 插件到底是怎麼實現的?
- 江同學的 2022 年終總結,請查收~
- kotlin-android-extensions 插件將被正式移除,如何無縫遷移?
- 學習一下 nowinandroid 的構建腳本
- Kotlin 默認可見性為 public,是不是一個好的設計?
- 2022年編譯加速的8個實用技巧
- 落地 Kotlin 代碼規範,DeteKt 瞭解一下~
- Gradle 進階(二):如何優化 Task 的性能?
- 開發一個支持跨平台的 Kotlin 編譯器插件
- 開發你的第一個 Kotlin 編譯器插件
- Kotlin 增量編譯是怎麼實現的?
- Gradle 都做了哪些緩存?
- K2 編譯器是什麼?世界第二高峯又是哪座?
- Android 性能優化之 R 文件優化詳解
- Kotlin 快速編譯背後的黑科技,瞭解一下~
- 別了 KAPT , 使用 KSP 快速實現 ButterKnife
- Android Apk 編譯打包流程,瞭解一下~
- 如何優雅地擴展 AGP 插件
- ASM 插樁採集方法入參,出參及耗時信息
- Transform 被廢棄,ASM 如何適配?