查看原文
其他

Gradle 与 AGP 构建 API: 如何编写插件

Android Android 开发者 2022-05-12
欢迎阅读 MAD Skills 系列之 Gradle 与 AGP 构建 API 的第二篇文章。通过上篇文章Gradle 与 AGP 构建 API: 配置您的构建文件您已经了解 Gradle 的基础知识以及如何配置 Android Gradle Plugin。在本文中,您将学习如何通过编写您自己的插件来扩展您的构建。如果您更喜欢通过视频了解此内容,请在此处查看: 

△ Gradle 与 AGP 构建 API: 如何编写插件

Android Gradle Plugin 从 7.0 版开始提供稳定的扩展点,用于操作变体配置和生成的构建产物。该 API 的一些部分是最近才完成的,因此我将会在本文中使用 7.1 版 AGP (撰写本文时尚处于 Beta 版)。



Gradle Task



我会从一个全新的项目开始。如果您想要同步学习,可以通过选择基础 Activity 模板来创建一个新项目。


让我们从创建 Task 并打印输出开始——没错,就是 hello world。为此,我会在应用层的 build.gradle.kts 文件注册一个新的 Task,并将其命名为 "hello"。 


tasks.register("hello"){ }


现在 Task 已经准备就绪,我们可以打印出 "hello" 并加上项目名称。注意当前 build.gradle.kts 文件属于应用模块,所以 project.name 将会是当前模块的名字 "app"。而如果我是用 project.parent?.name,就会返回项目的名称。

tasks.register("hello"){ println("Hello " + project.parent?.name)}


是时候运行该 Task 了。此时查看 Task 列表,可以看到我的 Task 已经位列其中。

△ 新的 Task 已经列在 Android Studio 的 Gradle 窗格中了

我可以双击 hello Task 或通过终端执行此 Task,并在构建输出中观察它所打印的 hello 信息。
△ Task 在构建输出中打印的 hello 信息
在查看日志时,我可以看到此信息是在配置阶段打印的。配置阶段实际上与执行 Task 的功能 (例如本例中的打印 Hello World) 无关。配置阶段是进行 Task 配置以作用于其执行的阶段。您可以在此阶段确定 Task 的输入、参数,以及输出的位置。

无论请求运行哪个 Task,配置阶段都会执行。在配置阶段执行耗时操作会导致较长的配置时间。

Task 的执行应当只在执行阶段发生,所以我们需要将打印调用移动至执行阶段。我可以通过添加 doFirst()doLast() 函数来达到这一目的,二者分别可以在执行阶段的开始和结束时打印 hello 消息。
tasks.register("hello"){ doLast { println("Hello " + project.parent?.name) }}


当我再次运行 Task 时,我可以看到 hello 信息是在执行阶段打印的。

△ 现在 Task 会在执行阶段打印 hello 信息
我的自定义 Task 目前位于 build.gradle.kts 文件中。添加自定义 Task 到 build.gradle 文件是创建自定义构建脚本的方便法门。不过,在我的插件代码变得愈发复杂时,这种方式不利于进行扩展。我们建议将自定义 Task 和插件实现放置于 buildSrc 文件夹。



在 buildSrc 中实现插件



在编写更多代码前,让我们将 hello Task 移动至 buildSrc。我会创建一个新的文件夹,并将其命名为 buildSrc。接下来,我为插件项目创建了一个 build.gradle.kts 文件,这样 Gradle 就会自动将此文件夹添加至构建。


这是项目根文件夹中的顶层目录。注意,我并不需要在我的项目中将其添加为模块。Gradle 会自动编译目录中的代码,并将其加入到您构建脚本的 classpath 中。


接下来,我创建了一个新的 src 文件夹与一个名为 HelloTask 的类。我将新的类改为 abstract 类,并使其继承 DefaultTask。随后,我会添加一个名为 taskAction 的函数、使用 @TaskAction 注解此函数,并将我自定义的 Task 代码迁移至此函数中。

abstract class HelloTask: DefaultTask() { @TaskAction fun taskAction() { println("Hello \"${project.parent?.name}\" from task!") }}


现在,我的 Task 已经就绪。我会创建一个新的插件类,这需要实现 Plugin 类型并覆盖 apply() 函数。Gradle 会调用此函数并传入 Project 对象。为了注册 HelloTask,我需要在 project.tasks 上调用 register(),并为这个新的 Task 命名。
class CustomPlugin: Plugin<Project> { override fun apply(project: Project) { project.tasks.register<HelloTask>("hello") }}


此时,我也可以将我的 Task 声明为依赖其他 Task。
class CustomPlugin: Plugin<Project> { override fun apply(project: Project) { project.tasks.register<HelloTask>("hello"){ dependsOn("build") } }}


下面让我们应用新的插件。注意,如果我的项目含有多个模块,我也可以通过将此插件加入其他 build.gradle 文件来复用它。

plugins { id ("com.android.application") id ("org.jetbrains.kotlin.android")}apply<CustomPlugin>()android { ...}


现在,我会运行 hello Task,并像之前一样观察插件的运行。

./gradlew hello

到目前为止,我已经将我的 Task 移至 buildSrc,让我们更进一步,探索新的 Android Gradle Plugin API。AGP 为其构建产物时的生命周期提供了扩展点。

在开始学习 Variant API 前,让我们先了解什么是 Variant。变体 (variant) 是您应用可以构建的不同版本。假设除了功能完整的应用,您还希望构建一个演示版的应用或用于调试的内部版本。您还可以针对不同的目标 API 或设备类型。变体由多个构建类型组合而成,例如 debugrelease,以及构建脚本中定义的产品变种。


  • Variant

    https://developer.android.google.cn/studio/build/build-variants


在您的构建文件中,使用声明式 DSL 添加构建类型是完全没有问题的。不过,在代码中以这种方式让您的插件影响构建是不可能的,或者说难以使用声明式语法进行表达。


AGP 通过解析构建脚本及 android 块中设置的属性来启动构建。新的 Variant API 回调让我可以从 androidComponents 扩展中添加 finalizeDSL() 回调。在此回调中,我可以在 DSL 对象应用于 Variant 创建前对它们进行修改。我将创建一个新的构建类型并且设置它的属性。

val extension = project.extensions.getByName( "androidComponents") as ApplicationAndroidComponentsExtension
extension.finalizeDsl { ext-> ext.buildTypes.create("staging").let { buildType -> buildType.initWith(ext.buildTypes.getByName("debug")) buildType.manifestPlaceholders["hostName"] = "example.com" buildType.applicationIdSuffix = ".debugStaging" }}


注意,在此阶段中,我可以创建或注册新的构建类型并设置它们的属性。在阶段结束时,AGP 将会锁定 DSL 对象,这样它们就无法再被更改。如果我再次运行构建,我会看到应用的 staging 版本被构建了。

现在,假设我的一个测试没有通过,这时我想要禁用单元测试来构建一个内部版本,以找出问题所在。

为了禁用单元测试,我可以使用 beforeVariants() 回调。该回调可以让我通过 VariantBuilder 对象进行这类修改。在这里,我会检查当前变体是否是我为 staging 创建的变体。接下来,我将禁用单元测试并设置不同的 minSdk 版本。
extension.beforeVariants { variantBuilder -> if (variantBuilder.name == "staging") { variantBuilder.enableUnitTest = false variantBuilder.minSdk = 23 }}


在此阶段后,组件列表和将要创建产物都会被确定。

本示例的完整代码如下。如需更多此类示例,请查阅 Github gradle-recipes 仓库:
https://github.com/android/gradle-recipes
import com.android.build.api.variant.ApplicationAndroidComponentsExtensionimport org.gradle.api.Pluginimport org.gradle.api.Project
class CustomPlugin: Plugin<Project> { override fun apply(project: Project) { project.tasks.register("hello"){ task-> task.doLast { println("Hello " + project.parent?.name) } }
val extension = project.extensions.getByName("androidComponents") as ApplicationAndroidComponentsExtension extension.beforeVariants { variantBuilder -> if (variantBuilder.name == "staging") { variantBuilder.enableUnitTest = false variantBuilder.minSdk = 23 } } extension.finalizeDsl { ext-> ext.buildTypes.create("staging").let { buildType -> buildType.initWith(ext.buildTypes.getByName("debug")) buildType.manifestPlaceholders["hostName"] = "internal.example.com" buildType.applicationIdSuffix = ".debugStaging" // 在后面解释 beforeVariants 时添加了本行代码。 buildType.isDebuggable = true } } }}


总结



编写您自己的插件,您可以扩展 Android Gradle Plugin 并根据您的项目需求自定义您的构建!

在本文中,您已经了解了如何使用新的 Variant API 来在 AndroidComponentsExtension 中注册回调、使用 DSL 对象初始化 Variant、影响已被创建的 Variant,以及在 beforeVariants() 中它们的属性。

在下一篇文章中,我们将进一步介绍 Artifacts API,并向您展示如何从您的自定义 Task 中读取和转换产物。

您也可以通过下方二维码向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!



推荐阅读

如页面未加载,请刷新重试

 点击屏末 | 阅读原文 | 即刻了解扩展 Android Gradle 插件更多内容



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存