查看原文
其他

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

Android Android 开发者 2022-05-12
欢迎阅读 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 插件更多内容



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

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