如何优雅地扩展 AGP 插件

前言

我们在项目中常常需要扩展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中需要通过以下方式处理:

    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文件,可以通过以下方法实现

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<VerifyManifestTask>("${name}VerifyManifest") {
        	// 获取merged_manifest并给Task设值
            mergedManifest.set(artifacts.get(ArtifactType.MERGED_MANIFEST))
        }
    }
}
复制代码

如上所示,通过以上方式,不需要知道依赖于哪个Task,也不需要了解Task的具体实现,就可以轻松获取MERGED_MANIFEST的内容

Transformation操作

Transformation即变换操作,它可以把原有产物作为输入值,进行变换后将结果作为输出值,并传递给下一个变换

下面我们来看一个将ManifestVersionCode替换为git head的变换示例:

// 获取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<GitVersionTask>("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 的列表,因此任务可以声明将附加到列表的输出。

// 声明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<SomeProducer>("${name}SomeProducer") {
              // ... configure your task as needed ...
        }
        artifacts.use(someProducer)
            .wiredWith(SomeProducerTask::output)
            .toAppendTo(ArtifactType.MANY_ARTIFACT)
    }
}
复制代码

Creation操作

此操作用一个新的 Artifact 替换当前的Artifact,丢弃所有以前的Artifact。这是一个“输出”操作,其中任务声明自己是产物的唯一提供者。如果有多个任务将自己声明为产物提供者,则最后一个将获胜。

例如,自定义任务可能不使用内置清单合并,而是通过代码写入一个新的。

// 声明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<ManifestProducerTask>("${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的使用了~

示例代码

本文所有代码可见:github.com/android/gra…

参考资料

New APIs in the Android Gradle Plugin
社区说|扩展 Android 构建流程 - 基于新版 Variant/Artifact APIs
扩展 Android Gradle 插件

猜你喜欢

转载自juejin.im/post/7111118062262321189
今日推荐