如何优雅地扩展 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 插件