深入探索 Android Gradle 插件的缓存配置
什么是配置缓存?
惰性配置 (lazy configuration) https://docs.gradle.org/current/userguide/lazy_configuration.html
性能改进
这一功能的主要目标便是提升构建速度。在 Android 版 Santa Tracker 工程的基准化分析中,对于启用了配置缓存的构建过程,我们测量出其在 Android Studio 中的总构建时间减少了 35% (从 688ms 到 443ms,测试平台为 Linux,使用 Intel® Xeon® Gold 6154 CPU @ 3.00GHz )。下图展示了使用和不使用配置缓存进行 100 次构建的平均总构建时间 (以毫秒为单位):
Santa Tracker
https://github.com/gradle/santa-tracker-performance
对于一些工程,配置阶段可能会消耗 10 秒钟以上,节省时间的效果也因此更加显著。无论运行的是全新构建、增量构建还是更新构建,配置阶段的开销都是相同的。要衡量您的构建过程中配置阶段所消耗的时间,可以以空运行模式 (dry run mode) 运行任务,例如: ./gradlew :app:assembleDebug --dry-run。
为了进一步避免重复运行配置过程,配置缓存还允许来自同一工程的任务并行运行。以前,只有利用 Worker API 的任务可以同时运行,但是由于配置缓存可以确保任务独立且无法访问全局共享状态 (例如 Project 实例),因此可以默认启用此行为。而且,依赖关系解析结果可以在运行间进行缓存,从而有助于优化整体构建时间。
Worker API
https://guides.gradle.org/using-the-worker-api/
Project
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html
如何试用?
issue
https://github.com/gradle/gradle/issues/13490
org.gradle.unsafe.configuration-cache=true
# 小心使用这一标记,因为有些插件还没有完全兼容
org.gradle.unsafe.configuration-cache-problems=warn
Kotlin issue
https://youtrack.jetbrains.com/issue/KT-33908
Android Studio issue 跟踪
https://developer.android.google.cn/studio/report-bugs
Gradle issue 跟踪
https://github.com/gradle/gradle/issues
它是如何工作的?
Project.afterEvaluate
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#afterEvaluate-org.gradle.api.Action-
在评估 DSL 以及注册任务之后,接下来的阶段会构建一个任务图。您所要求执行的任务以及它们所依赖的任务都会被完全配置。这一过程将会持续到触达没有依赖的叶子任务为止。配置的这一阶段将会输出一个任务图,Gradle 中的调度机制会使用该任务图来运行构建操作。当任务图被完成后,配置缓存会将其存储在磁盘中 (在 Gradle 6.6 中位于根工程的 .gradle/configuration-cache directory 目录下) 。它可以序列化所有的 Gradle-managed 类型 (如 FileCollection、Property、Provider) 以及所有用户定义的可序列化类型。在此阶段结束时,每个任务的状态都将被完全记录并保留下来。
FileCollection https://docs.gradle.org/current/javadoc/org/gradle/api/file/FileCollection.html Property https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Property.html Provider https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html
使用兼容的 Gradle API
在任务中使用 Project 实例
Gradle 插件中最常见的兼容性问题来自于在任务操作中使用 Task.getProject()。在使用配置缓存时,为了保持每个任务完全独立,任务将无法访问这一共享状态。由于 Project 实例可以访问 TaskContainer、ConfigurationContainer 以及其他在启用缓存的运行期间不会填充的对象,从而导致反映出无效的状态,所以禁用它是必须的。引入了很多可替代的 API,比如用于延迟对象创建的 ObjectFactory,还有可以用于获取项目文件系统分布情况的接口,比如 ProjectLayout,如果需要在构建中启动进程,可以使用 ExecOperations。您可以参考完整的 API 列表来进行迁移工作。
Task.getProject()
https://docs.gradle.org/current/javadoc/org/gradle/api/Task.html#getProject--
Project
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html
TaskContainer
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#getTasks--
ConfigurationContainer
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#getConfigurations--
ObjectFactory
https://docs.gradle.org/current/javadoc/org/gradle/api/model/ObjectFactory.html
ProjectLayout
https://docs.gradle.org/current/javadoc/org/gradle/api/file/ProjectLayout.html
ExecOperations
https://docs.gradle.org/current/javadoc/org/gradle/process/ExecOperations.html
完整的 API 列表
https://docs.gradle.org/6.6-rc-1/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution
访问 Gradle/系统 属性与环境变量
如果您使用系统属性、Gradle 属性、环境变量或者额外文件来指定构建的逻辑输入时,会产生怎样的结果?构建系统已经在跟踪 build 文件的修改,但是任何影响任务图的额外值都应当使用 ProviderFactory API 进行获取。下面的示例展示了如何获取影响配置的 enableTask 系统属性值,以及如何获取仅作为任务输入的系统属性 anotherFlag。如果前者的值发生改变,则缓存失效;而如果后者的值改变,则缓存会被复用,而任务也不会处于最新的状态:
val systemProperty = project.providers.systemProperty("enableTask").forUseAtConfigurationTime()
if (systemProperty.orNull == "enabled") {
project.tasks.register("myTask", …) {
it.anotherFlag.set(project.providers.systemProperty("anotherFlag"))
}
}
ProviderFactory https://docs.gradle.org/current/javadoc/org/gradle/api/provider/ProviderFactory.html
在内部,Gradle 会对在配置阶段解析的值提供者 (value provider) 进行持续跟踪,每个值提供者都会被视为一个构建逻辑输入。另外,除非调用 Provider.forUseAtConfigurationTime(),否则无法解析提供者,从而使得意外引入配置阶段输入的情况很难发生。如前文所述,任何 Gradle 会在 build 文件发生改变时使配置缓存失效,这一特性与 ProviderFactory API 一起确保了 Gradle 可以捕获影响任务图的所有内容。
Provider.forUseAtConfigurationTime() https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#forUseAtConfigurationTime--
如果您希望可以在任务间共享一些工作,例如: 避免多次连接到网络服务器或者避免多次解析某些信息,那么可以使用兼容配置缓存的共享构建服务来进行实现。就像任务一样,构建服务可以包含输入信息,并且这些内容会在第一次运行后序列化。缓存的运行将会简单地反序列化参数并实例化任务所需的构建服务。构建服务的额外好处是它与构建生命周期非常契合,如果您希望在构建完成后释放一些资源,那么在您的构建服务中使用 AutoCloseable 便可以实现这一功能。由于无法被安全地序列化至磁盘,添加构建监听的操作与配置缓存不兼容。
共享构建服务 https://docs.gradle.org/current/userguide/build_services.html AutoCloseable https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html?is-external=true
从迁移 Android Gradle 插件获得的经验教训
在努力使 Android Gradle 插件兼容配置缓存的过程中,我们学到了一些可能对插件和脚本作者有用的东西。
首先,在启用配置缓存后,如果在构建输出中看到下面这样的内容,不要气馁,因为许多问题都是重复的,可以轻松解决:
428 problems were found reusing the configuration cache, 4 of which seem unique.
(在复用配置缓存后,发现了 428 处问题,其中 4 处看起来比较特别)
通过迁移到新的 API,我们可以轻松解决许多问题。例如:
abstract class MyTask: DefaultTask() {
@TaskAction
fun process() {
project.exec(…)
project.logger().log(…)
}
}
abstract class MyTask: DefaultTask() {
@get:Inject
abstract val execOperations: ExecOperations
@TaskAction
fun process() {
execOperations.exec(…)
this.logger.log(…)
}
}
如果您仍在任务中使用 Project 实例,那么您需要找到一个替代 API。对于大多数情况,都会有一个兼容的 API,您只需直接迁移即可。
另一个方便之处是避免了在任务创建时创建不可序列化或者开销昂贵的对象,作为替代,会在我们的任务操作中需要时才创建它们。例如,在下面的示例中,我们不必强制要求 Handler 类型可被序列化,因为我们仅在需要时才创建它:
abstract class Mytask: DefaultTask() {
private val handler: Handler by lazy { createHandler(someInput) }
@TaskAction
fun process() {
handler.doSomething(…)
}
}
abstract class Mytask: DefaultTask() {
@TaskAction
fun process() {
val handler = createHandler(someInput)
}
}
Project
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html
FileCollection
https://docs.gradle.org/current/javadoc/org/gradle/api/file/FileCollection.html
abstract class MyTask: DefaulTask() {
private val userConfiguration: MyDslObjects
@InputFiles
fun getClasses(): FileCollection {
return project.configurations.getByName(userConfiguration.name)
}
@Internal
fun getBuildDir(): File {
return project.buildDir
}
@TaskAction
fun process() { … }
}
abstract class MyTask: DefaulTask() {
@get:InputFiles
abstract val classes: ConfigurableFileCollection
@get:Internal
abstract val buildDir: DirectoryProperty
@TaskAction
fun process() { … }
}
project.tasks.register("myTask", MyTask::class.java) {
it.classes.from(project.configurations.getByName(userConfiguration.name))
it.buildDir.set(project.layout.buildDirectory)
}
Android Gradle 插件曾依赖的一种常见模式,是在首次使用时初始化一些对象,将其存储在静态字段中,并利用构建监听器在构建完成时清除这些状态。正如上文所述,针对这种用例应当使用共享构建服务。请参阅下面的示例以了解如何使用它:
abstract MyBuildService: BuildService<BuildServiceParameters.None>, AutoCloseable {
fun doAndCacheSomeComplexWork() { ... }
override fun close() {
// 清除所有状态,释放内存
}
}
abstract class MyTask: DefaultTask() {
@get:Internal
abstract val myService: Property<MyBuildService>
}
共享构建服务 https://docs.gradle.org/current/userguide/build_services.html
最后一条建议是,当您实现自定义可序列化类型时,要注意被序列化的内容。确保不要序列化派生属性,并让这些属性成为临时的或使用函数作为替代。举例来说,在缓存运行时,您将会为 allLines 属性获取到一个旧的值,因此这一操作是必须的。
class StringsFromFiles(private val inputs: FileCollection) {
val allLines = inputFiles.files.flatMap { it.readLines() }
}
class StringsFromFiles(private val inputs: FileCollection): Serializable {
fun getAllLines() {
return inputFiles.files.flatMap { it.readLines() }
}
}
配置缓存目前还处于实验阶段,我们希望您可以尝试并向我们提供反馈。您可以通过 Android Studio issue 跟踪或 Gradle 的 issue 跟踪向我们报告您所遇到的任何问题。
Android Studio issue 跟踪
https://developer.android.google.cn/studio/report-bugs
Gradle 的 issue 跟踪
https://github.com/gradle/gradle/issues
编码愉快!
推荐阅读