Gradle 与 AGP 构建 API: 进一步完善您的插件!

欢迎阅读 MAD Skills 系列 之 Gradle 与 AGP 构建 API 的第三篇文章。在上一篇文章《Gradle 与 AGP 构建 API: 如何编写插件》中,您学习了如何编写您自己的插件,以及如何使用 Variants API

如果您更喜欢通过视频了解此内容,请在此处查看:

△ Gradle 与 AGP 构建 API: 进一步完善您的插件

在本文中,您将会学习 Gradle 的 Task、Provider、Property 以及使用 Task 进行输入与输出。同时您也将进一步完善您的插件,并学习如何使用新的 Artifact API 访问各种构建产物。

Property

假设我想要创建一个插件,该插件可以使用 Git 版本自动更新应用清单文件中指定的版本号。为了达到这一目标,我需要为构建添加两个 Task。第一个 Task 会获取 Git 版本,而第二个 Task 将会使用该 Git 版本来更新清单文件。

让我们从创建名为 GitVersionTask 的新任务开始。GitVersionTask 需要继承 DefaultTask,同时实现带有注解的 taskAction 函数。下面是查询 Git 树顶端信息的代码。

abstract class GitVersionTask: DefaultTask() {
   @TaskAction
   fun taskAction(){
       // 这里是获取树版本顶端的代码
       val process = ProcessBuilder(
           "git",
           "rev-parse --short HEAD"
       ).start()
       val error = process.errorStream.readBytes().toString()
       if (error.isNotBlank()) {
           System.err.println("Git error : $error")
       }
       var gitVersion = process.inputStream.readBytes().toString()
       //...
   }
}

我不能直接缓存版本信息,因为我想将它存储在一个中间文件中,从而让其他 Task 也可以读取和使用这个值。为此,我需要使用 RegularFileProperty。Property 可以用于 Task 的输入与输出。在本例中,Property 将会作为呈现 Task 输出的容器。我创建了一个 RegularFileProperty,并使用 @get:OutputFile 对其进行注解。OutputFile 是附加至 getter 函数的标记注解。此注解会将 Property 标记为该 Task 的输出文件。

@get:OutputFile
abstract val gitVersionOutputFile: RegularFileProperty

现在,我已经声明了 Task 的输出,让我们回到 taskAction() 函数,我会在这里访问文件并写入我想要存储的文本。本例中,我会存储 Git 版本,也就是 Task 的输出。为了简化示例,我将查询 Git 版本的代码替换为了硬编码字符串。

abstract class GitVersionTask: DefaultTask() {
   @get:OutputFile
   abstract val gitVersionOutputFile: RegularFileProperty
   @TaskAction
   fun taskAction() {
       gitVersionOutputFile.get().asFile.writeText("1234")
   }
}

现在,Task 已经准备就绪,让我们在插件代码中对其进行注册。首先,我会创建一个名为 ExamplePlugin 的新插件类,并在其中实现 Plugin。如果您不熟悉在 buildSrc 文件夹中创建插件的流程,可以回顾本系列的前两篇文章:《Gradle 与 AGP 构建 API: 配置您的构建文件》、《Gradle 与 AGP 构建 API: 如何编写插件》。

△ buildSrc 文件夹

△ buildSrc 文件夹

接下来我会注册 GitVersionTask 并将文件 Property 设置为输出到 build 文件夹中的一个中间文件上。我同时还将 upToDateWhen 设置为 false,这样此 Task 前一次执行的输出就不会被复用。这也意味着由于该 Task 不会处于最新的状态,因此每次构建时都会被执行。

override fun apply(project: Project) {
   project.tasks.register(
       "gitVersionProvider",
       GitVersionTask::class.java
   ) {
       it.gitVersionOutputFile.set(
           File(
               project.buildDir,  
               "intermediates/gitVersionProvider/output"
           )
       )
       it.outputs.upToDateWhen { false }
    }
}

在 Task 执行完毕后,我就可以检查位于 build/intermediates 文件夹下的 output 文件了。我只要验证 Task 是否存储了我所硬编码的值即可。

接下来让我们转向第二个 Task,该 Task 会更新清单文件中的版本信息。我将它命名为 ManifestTransformTask,并使用两个 RegularFileProperty 对象作为它的输入值。

abstract class ManifestTransformerTask: DefaultTask() {
   @get:InputFile
   abstract val gitInfoFile: RegularFileProperty
   @get:InputFile
   abstract val mergedManifest: RegularFileProperty
}

我会用第一个 RegularFileProperty 读取 GitVersionTask 生成的输出文件中的内容;用第二个 RegularFileProperty 读取应用的清单文件。然后我就可以用 gitInfoFile 文件中 gitVersion 变量所存储的版本号替换清单文件中的版本号了。

@TaskAction
fun taskAction() {
   val gitVersion = gitInfoFile.get().asFile.readText()
   var manifest = mergedManifest.asFile.get().readText()
   manifest = manifest.replace(
       "android:versionCode=\"1\"",    
       "android:versionCode=\"${gitVersion}\""
   )
  
}

现在,我可以写入更新后的清单文件了。首先,我会为输出创建另一个 RegularFileProperty,并使用 @get:OutputFile 对其进行注解。

@get:OutputFile
abstract val updatedManifest: RegularFileProperty

注意: 我本可以使用 VariantOutput 直接设置 versionCode,而无需重写清单文件。但是为了向您展示如何使用构建产物转换,我会通过本示例的方式得到相同的效果。

让我们回到插件,并将一切联系起来。我首先获得 AndroidComponentsExtension。我希望在 AGP 决定创建哪个变体后、在各种对象的值被锁定而无法被修改之前执行这一新 Task。onVariants() 回调会在 beforeVariants() 回调后调用,后者可能会让您想起 前一篇文章

val androidComponents = project.extensions.getByType(
   AndroidComponentsExtension::class.java
)
androidComponents.onVariants { variant ->
   //...
}

Provider

您可以使用 Provider 连接 Property 到其他需要执行耗时操作 (例如读取文件或网络等外部输入) 的 Task。

我会从注册 ManifestTransformerTask 开始。此 Task 依赖 gitVersionOutput 文件,而该文件是前一个 Task 的输出。我将通过使用 Provider 来访问这一 Property

val manifestUpdater: TaskProvider = project.tasks.register(
   variant.name + "ManifestUpdater",  
   ManifestTransformerTask::class.java
) {
   it.gitInfoFile.set(
       //...
   )
}

Provider 可以用于访问指定类型的值,您可以直接使用 get() 函数,也可以使用操作符函数 (如 map()flatMap()) 将值转换为新的 Provider。在我回顾 Property 接口时,发现其实现了 Property 接口。您可以将值惰性地设置给 Property,并在稍候惰性地使用 Provider 访问这些值。

当我查看 register() 的返回类型时,发现它返回了给定类型的 TaskProvider。我将其赋值给了一个新的 val

val gitVersionProvider = project.tasks.register(
   "gitVersionProvider",
   GitVersionTask::class.java
) {
   it.gitVersionOutputFile.set(
       File(
           project.buildDir,
           "intermediates/gitVersionProvider/output"
       )
    )
    it.outputs.upToDateWhen { false }
}

现在我们回过头来设置 ManifestTransformerTask 的输入。在我尝试将来自 Provider 的值映射为输入 Property 时,产生了一个错误。map() 的 lambda 参数接收某种类型 (如 T) 的值,该函数会产生另一个类型 (如 S) 的值。

△ 使用 map() 时造成的错误

△ 使用 map() 时造成的错误

然而,在本例中,set 函数需要 Provider 类型。我可以使用 flatMap() 函数,该函数也接收一个 T 类型的值,但会产生一个 S 类型的 Provider,而不是直接产生 S 类型的值。

it.gitInfoFile.set(
   gitVersionProvider.flatMap(
       GitVersionTask::gitVersionOutputFile
   )
)

转换

接下来,我需要告诉变体的产物使用 manifestUpdater,同时将清单文件作为输入,将更新后的清单文件作为输出。最后,我调用 toTransform() 函数转换单个产物的类型。

variant.artifacts.use(manifestUpdater)
  .wiredWithFiles(
      ManifestTransformerTask::mergedManifest,
      ManifestTransformerTask::updatedManifest
  ).toTransform(SingleArtifact.MERGED_MANIFEST)

在运行此 Task 时,我可以看到应用清单文件中的版本号被更新成了 gitVersion 文件中的值。需要注意的是,我并没有显式地要求 GitProviderTask 运行。该任务之所以被执行,是因为其输出是 ManifestTransformerTask 的输入,而后者是我所请求运行的。

BuiltArtifactsLoader

让我们添加另一个 Task,来了解如何访问已被更新的清单文件并验证它是否被更新成功。我会创建一个名为 VerifyManifestTask 的新任务。为了读取清单文件,我需要访问 APK 文件,该文件是构建 Task 的产物。为此,我需要将构建 APK 文件夹作为 Task 的输入。

注意,这次我使用了 DirectoryProperty 而不是 FileProperty,因为 SingleArticfact.APK 对象可以表示构建之后存放 APK 文件的目录。

我还需要一个类型为 BuiltArtifactsLoader 的 Property 作为 Task 的第二个输入,我会用它从元数据文件中加载 BuiltArtifacts 对象。元数据文件描述了 APK 目录下的文件信息。若您的项目包含原生组件、多种语言等要素,那么每次构建都可以产生数个 APK。BuiltArtifactsLoader 抽象了识别每个 APK 及其属性 (如 ABI 和语言) 的过程。

@get:Internal
abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>

是时候实现 Task 了。首先我加载了 buildArtifacts,并保证其中只包含了一个 APK,接着将此 APK 作为 File 实例进行加载。

val builtArtifacts = builtArtifactsLoader.get().load(
   apkFolder.get()
)?: throw RuntimeException("Cannot load APKs")
if (builtArtifacts.elements.size != 1)
  throw RuntimeException("Expected one APK !")
val apk = File(builtArtifacts.elements.single().outputFile).toPath()

这时,我已经可以访问 APK 中的清单文件并验证版本是否已经更新成功。为了保持示例的简洁,我在这里只会检查 APK 是否存在。我还添加了一个 “在此处检查清单文件” 的提醒,并打印了成功的信息。

println("Insert code to verify manifest file in ${apk}")
println("SUCCESS")

现在我们回到插件的代码以注册此 Task。在插件代码中,我将此 Task 注册为 “Verifier”,并传入 APK 文件夹和当前变体产物的 buildArtifactLoader 对象。

project.tasks.register(
   variant.name + "Verifier",
   VerifyManifestTask::class.java
) {
   it.apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
   it.builtArtifactsLoader.set(
       variant.artifacts.getBuiltArtifactsLoader()
   )
}

当我再次运行 Task 时,可以看到新的 Task 加载了 APK 并打印了成功信息。注意,这次我依旧没有显式请求清单转换的执行,但是因为 VerifierTask 请求了最终版本的清单产物,所以自动进行了转换。

总结

我的 插件 中包含三个 Task: 首先,插件会检查当前 Git 树,并将版本存储在一个中间文件中;随后,插件会惰性使用上一步的输出,并使用一个 Provider 将版本号更新至当前的清单文件;最后,插件会使用另一个 Task 访问构建产物,并检查清单文件是否正确更新。

以上就是全部内容!从 7.0 版开始,Android Gradle 插件提供了官方的扩展点,以便您编写自己的插件。使用这些新 API,您可以控制构建输入、读取、修改甚至替换中间和最终产物。

如需了解更多内容,学习如何保持您构建的高效性,请查阅 官方文档gradle-recipes

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