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

Gradle 与 AGP 构建 API: 进一步完善您的插件!
www.zeeklog.com  - 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 文件夹中创建插件的流程,可以回顾本系列的前两篇文章:《》、《》。

www.zeeklog.com  - 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) 的值。

www.zeeklog.com  - Gradle 与 AGP 构建 API: 进一步完善您的插件!

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

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

www.zeeklog.com  - Gradle 与 AGP 构建 API: 进一步完善您的插件!

推荐阅读

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

www.zeeklog.com  - Gradle 与 AGP 构建 API: 进一步完善您的插件!

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


www.zeeklog.com  - Gradle 与 AGP 构建 API: 进一步完善您的插件!
www.zeeklog.com  - Gradle 与 AGP 构建 API: 进一步完善您的插件!
www.zeeklog.com  - Gradle 与 AGP 构建 API: 进一步完善您的插件!

Read more

Mysql 数据库优化秘籍

Mysql 数据库优化秘籍

文章目录 * * Mysql 数据库优化秘籍 “ SQL优化在提升系统性能中扮演着举足轻重的角色,已经成为衡量程序猿优秀与否的硬性指标,甚至在各大厂招聘岗位职能上都有明码标注。如果是你,在这个问题上能吊打面试官还是会被吊打呢?” 调优原则 — 倒金字塔 数据库优化是个亘古存在的问题,在数据库角度可以分成 SQL/索引优化、表结构优化、系统配置优化、硬件优化,四层倒金字塔结构。 图片 更多细节请关注 公众号 ““! 金字塔结构中各级优化成本逐级递增,效果逐级递减!“SQL&索引层“ 和 ”表结构层“ 与程序设计实现强相关,ROI[投资回报率]最高。 故,调优涉及通常更多的是在 “SQL&索引层“ 和 ”表结构层“ 做调整。 SQL 语法/执行顺序 理解SQL优化原理之前 ,首先要搞清楚SQL一些重要的顺序。 2.1 语法顺序 在业务实现中,良好的

By Ne0inhk
Redis的进程调度演进史

Redis的进程调度演进史

文章目录 * * Redis的进程调度演进史 Redis作为一个基于内存的缓存系统,一直以高性能著称,因没有上下文切换以及无锁操作,即使在单线程处理情况下,读速度仍可达到11万次/s,写速度达到8.1万次/s。但是,单线程的设计也给Redis带来一些问题: 只能使用CPU一个核; 如果删除的键过大(比如Set类型中有上百万个对象),会导致服务端阻塞好几秒; QPS难再提高。 针对上面问题,Redis在4.0版本以及6.0版本分别引入了Lazy Free以及多线程IO,逐步向多线程过渡,下面将会做详细介绍。 单线程原理 都说Redis是单线程的,那么单线程是如何体现的?如何支持客户端并发请求的?为了搞清这些问题,首先来了解下Redis是如何工作的。 Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件: 文件事件:Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象;服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作,比如连接accept,

By Ne0inhk
快速理解负载均衡

快速理解负载均衡

文章目录 * * * * * 快速理解负载均衡 一、负载均衡简介 了解更多,关注  ! 1.1. 大型网站面临的挑战 大型网站都要面对庞大的用户量,高并发,海量数据等挑战。为了提升系统整体的性能,可以采用垂直扩展和水平扩展两种方式。 垂直扩展:在网站发展早期,可以从单机的角度通过增加硬件处理能力,比如 CPU 处理能力,内存容量,磁盘等方面,实现服务器处理能力的提升。但是,单机是有性能瓶颈的,一旦触及瓶颈,再想提升,付出的成本和代价会极高。这显然不能满足大型分布式系统(网站)所有应对的大流量,高并发,海量数据等挑战。 水平扩展:通过集群来分担大型网站的流量。集群中的应用服务器(节点)通常被设计成无状态,用户可以请求任何一个节点,这些节点共同分担访问压力。水平扩展有两个要点: 应用集群:将同一应用部署到多台机器上,组成处理集群,接收负载均衡设备分发的请求,进行处理,并返回相应数据。 负载均衡:将用户访问请求,通过某种算法,

By Ne0inhk
Kafka ETL 的应用及架构解析|告别 Kafka Streams,让轻量级流处理更加简单

Kafka ETL 的应用及架构解析|告别 Kafka Streams,让轻量级流处理更加简单

文章目录 * * * Kafka ETL 的应用及架构解析|告别 Kafka Streams,让轻量级流处理更加简单 引言:阿里云消息队列 Kafka 版提供兼容 Apache Kafka 生态的全托管服务,彻底解决开源产品长期的痛点,是大数据生态中不可或缺的产品之一。随着 Kafka 越来越流行,最初只是作为简单的消息总线,后来逐渐成为数据集成系统,Kafka 可靠的传递能力让它成为流式处理系统可靠的数据来源。在大数据工程领域,Kafka 在承接上下游、串联数据流管道方面发挥了重要作用,Kafka 应用流式框架处理消息也逐渐成为趋势。 了解更多,请关注 公众号 “ “ ! 消息流处理框架选型 说到流计算,常用的便是 Storm、Spark Streaming 和 Flink,目前这些框架都已经完美的支持流计算,并且都有相应的使用案例,但这些框架使用起来门槛相对较高,首先要学习框架和各种技术、规范的使用,然后要将业务迁移到这些框架中,最后线上使用、运维这些流计算框架,对于简单的流处理应用来说,

By Ne0inhk