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

欢迎阅读 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 窗格中了

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

我可以双击 hello Task 或通过终端执行此 Task,并在构建输出中观察它所打印的 hello 信息。

△ 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 会在执行阶段打印 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 或设备类型。变体由多个构建类型组合而成,例如 debug 与 release,以及构建脚本中定义的产品变种。

在您的构建文件中,使用声明式 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 仓库:

import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import org.gradle.api.Plugin
import 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 中读取和转换产物。

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!