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

2022 年 1 月 5 日 谷歌开发者
欢迎阅读 MAD Skills 系列  Gradle 与 AGP 构建 API 的第三篇文章。在上一篇文章 Gradle 与 AGP 构建 API: 如何编写插件 中,您学习了如何编写您自己的插件,以及如何使用  Variants API :
https://developer.android.google.cn/studio/build/extend-agp#variant-api-artifacts-tasks

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

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

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


  • Artifact API

    https://developer.android.google.cn/studio/build/extend-agp#variant-api-artifacts-tasks



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:OutputFileabstract val gitVersionOutputFile: RegularFileProperty

  • RegularFileProperty
    https://docs.gradle.org/current/javadoc/org/gradle/api/file/RegularFileProperty.html

  • Property
    https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Property.html

  • OutputFile
    https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/OutputFile.html


现在,我已经声明了 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 文件夹
  • Plugin
    https://docs.gradle.org/current/javadoc/org/gradle/api/Plugin.html


接下来我会注册 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 变量所存储的版本号替换清单文件中的版本号了。
@TaskActionfun 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:OutputFileabstract val updatedManifest: RegularFileProperty


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

https://developer.android.google.cn/reference/tools/gradle-api/7.1/com/android/build/api/variant/VariantOutput?hl=en#versionCode:org.gradle.api.provider.Property


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

  • AndroidComponentsExtension
    https://developer.android.google.cn/reference/tools/gradle-api/7.0/com/android/build/api/extension/AndroidComponentsExtension



Provider


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


  • Provider
    https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html


我会从注册 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 访问这些值。


  • get()
    https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#get--

  • map()
    https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#map-org.gradle.api.Transformer-

  • flatMap()
    https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#flatMap-org.gradle.api.Transformer-


当我查看 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 }}

  • register()
    https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/TaskContainer.html#register-java.lang.String-

  • TaskProvider
    https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/TaskProvider.html


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

△  使用 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)

  • toTransform()
    https://developer.android.google.cn/reference/tools/gradle-api/7.1/com/android/build/api/artifact/InAndOutFileOperationRequest#toTransform(com.android.build.api.artifact.InAndOutFileOperationRequest.toTransform.ArtifactTypeT)


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



BuiltArtifactsLoader


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

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


  • DirectoryProperty
    https://docs.gradle.org/current/javadoc/org/gradle/api/file/DirectoryProperty.html

  • FileProperty
    https://docs.gradle.org/current/javadoc/org/gradle/api/file/RegularFileProperty.html

  • SingleArticfact.APK

    https://developer.android.google.cn/reference/tools/gradle-api/7.0/com/android/build/api/artifact/SingleArtifact.APK


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

@get:Internalabstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>

  • BuiltArtifactsLoader
    https://developer.android.google.cn/reference/tools/gradle-api/7.0/com/android/build/api/variant/BuiltArtifactsLoader

  • BuiltArtifacts
    https://developer.android.google.cn/reference/tools/gradle-api/7.0/com/android/build/api/variant/BuiltArtifacts


是时候实现 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 访问构建产物,并检查清单文件是否正确更新。


  • 插件
    https://github.com/android/gradle-recipes/blob/agp-7.1/BuildSrc/manifestUpdaterTest/buildSrc/src/main/kotlin/ExamplePlugin.kt

  • 首先
    https://github.com/android/gradle-recipes/blob/agp-7.1/BuildSrc/manifestUpdaterTest/buildSrc/src/main/kotlin/GitVersion.kt

  • 随后
    https://github.com/android/gradle-recipes/blob/agp-7.1/BuildSrc/manifestUpdaterTest/buildSrc/src/main/kotlin/ManifestTransformerTask.kt

  • 最后
    https://github.com/android/gradle-recipes/blob/agp-7.1/BuildSrc/manifestUpdaterTest/buildSrc/src/main/kotlin/VerifyManifestTask.kt


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

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


  • 官方文档
    https://developer.android.google.cn/studio/build/extend-agp

  • gradle-recipes
    https://github.com/android/gradle-recipes


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



推荐阅读

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


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




登录查看更多
0

相关内容

应用程序接口(简称 API),又称为应用编程接口,就是软件系统不同组成部分衔接的约定。
专知会员服务
6+阅读 · 2021年8月7日
专知会员服务
90+阅读 · 2020年12月26日
专知会员服务
38+阅读 · 2020年9月6日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
68+阅读 · 2020年1月17日
【GitHub实战】Pytorch实现的小样本逼真的视频到视频转换
专知会员服务
35+阅读 · 2019年12月15日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
20+阅读 · 2019年11月7日
使用 Compose 构建 Wear OS 应用
谷歌开发者
0+阅读 · 2022年3月17日
Flutter 如何与 Native (Android) 进行交互 | 开发者说·DTalk
Android Studio 新特性详解
谷歌开发者
0+阅读 · 2022年1月19日
在 Android 12 中构建更现代的应用 Widget
谷歌开发者
0+阅读 · 2022年1月12日
Gradle 与 AGP 构建 API: 如何编写插件
谷歌开发者
0+阅读 · 2021年12月24日
Gradle 与 AGP 构建 API: 配置您的构建文件
谷歌开发者
0+阅读 · 2021年12月21日
使用 CameraX Extensions API 将特效应用到照片上
谷歌开发者
0+阅读 · 2021年10月27日
Hilt 工作原理 | MAD Skills
谷歌开发者
0+阅读 · 2021年10月18日
导航: 多返回栈 | MAD Skills
谷歌开发者
0+阅读 · 2021年9月24日
R工程化—Rest API 之plumber包
R语言中文社区
11+阅读 · 2018年12月25日
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2013年12月31日
国家自然科学基金
1+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
7+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2010年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
VIP会员
相关VIP内容
专知会员服务
6+阅读 · 2021年8月7日
专知会员服务
90+阅读 · 2020年12月26日
专知会员服务
38+阅读 · 2020年9月6日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
68+阅读 · 2020年1月17日
【GitHub实战】Pytorch实现的小样本逼真的视频到视频转换
专知会员服务
35+阅读 · 2019年12月15日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
20+阅读 · 2019年11月7日
相关资讯
使用 Compose 构建 Wear OS 应用
谷歌开发者
0+阅读 · 2022年3月17日
Flutter 如何与 Native (Android) 进行交互 | 开发者说·DTalk
Android Studio 新特性详解
谷歌开发者
0+阅读 · 2022年1月19日
在 Android 12 中构建更现代的应用 Widget
谷歌开发者
0+阅读 · 2022年1月12日
Gradle 与 AGP 构建 API: 如何编写插件
谷歌开发者
0+阅读 · 2021年12月24日
Gradle 与 AGP 构建 API: 配置您的构建文件
谷歌开发者
0+阅读 · 2021年12月21日
使用 CameraX Extensions API 将特效应用到照片上
谷歌开发者
0+阅读 · 2021年10月27日
Hilt 工作原理 | MAD Skills
谷歌开发者
0+阅读 · 2021年10月18日
导航: 多返回栈 | MAD Skills
谷歌开发者
0+阅读 · 2021年9月24日
R工程化—Rest API 之plumber包
R语言中文社区
11+阅读 · 2018年12月25日
相关基金
国家自然科学基金
0+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2014年12月31日
国家自然科学基金
1+阅读 · 2013年12月31日
国家自然科学基金
1+阅读 · 2013年12月31日
国家自然科学基金
0+阅读 · 2012年12月31日
国家自然科学基金
7+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2012年12月31日
国家自然科学基金
1+阅读 · 2010年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
国家自然科学基金
0+阅读 · 2009年12月31日
Top
微信扫码咨询专知VIP会员