如何优雅地扩展 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
的使用了~
示例代码
本文所有代码可见:http://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 如何适配?