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

Posted 谷歌开发者

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Gradle 与 AGP 构建 API: 进一步完善您的插件!相关的知识,希望对你有一定的参考价值。

欢迎阅读 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:OutputFile
abstract 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 变量所存储的版本号替换清单文件中的版本号了。

@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,而无需重写清单文件。但是为了向您展示如何使用构建产物转换,我会通过本示例的方式得到相同的效果。

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

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

@get:Internal
abstract 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 插件更多内容


以上是关于Gradle 与 AGP 构建 API: 进一步完善您的插件!的主要内容,如果未能解决你的问题,请参考以下文章

Gradle 与 AGP 构建 API: 配置您的构建文件

Gradle 与 AGP 构建 API: 配置您的构建文件

AGP 3.5是否与Gradle 6兼容?

AGP 3.5 与 Gradle 6 兼容吗?

ASError:Android Gradle plugin requires Java 11

补齐Android技能树——从AGP构建过程到APK打包过程