如何優雅地擴充套件 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 如何適配?