配置缓存是一个提高 IDE 和命令行构建速度的基础构建块。这是 Gradle 6.6 版本提供的一个高度实验性功能,它可使构建系统记录一次任务的图谱信息,并在接下来的构建中进行复用,从而避免再一次配置整个工程。这一功能也是配置阶段改进的延续,这些改进中引入了 惰性配置 (lazy configuration),以免在构建的配置阶段进行没必要要的工做。这些改进对于快速迭代开发的重要性不言自明,然后者也是 Android Studio 团队所持续关注的一个用例。html
这一功能的主要目标即是提高构建速度。在 Android 版 Santa Tracker 工程的基准化分析中,对于启用了配置缓存的构建过程,咱们测量出其在 Android Studio 中的总构建时间减小了 35% (从 688ms 到 443ms,测试平台为 Linux,使用 Intel® Xeon® Gold 6154 CPU @ 3.00GHz )。下图展现了使用和不使用配置缓存进行 100 次构建的平均总构建时间 (以毫秒为单位):java
对于一些工程,配置阶段可能会消耗 10 秒钟以上,节省时间的效果也所以更加显著。不管运行的是全新构建、增量构建仍是更新构建,配置阶段的开销都是相同的。要衡量您的构建过程当中配置阶段所消耗的时间,能够以空运行模式 (dry run mode) 运行任务,例如: ./gradlew :app:assembleDebug --dry-run
。android
为了进一步避免重复运行配置过程,配置缓存还容许来自同一工程的任务并行运行。之前,只有利用 Worker API 的任务能够同时运行,可是因为配置缓存能够确保任务独立且没法访问全局共享状态 (例如 Project 实例),所以能够默认启用此行为。并且,依赖关系解析结果能够在运行间进行缓存,从而有助于优化总体构建时间。git
配置缓存功能如今还处于实验阶段,咱们但愿您能够尝试它并向咱们提供反馈。为了在您的构建中使用它,须要保证全部工程所应用的全部插件都是兼容的,这是为了安全地 (反) 序列化任务图。您可能须要更新某些 Gradle 插件。您能够经过此 issue 来获取受支持插件的完整列表,若是您使用的插件不在其中,请在它们的问题跟踪器中提交问题,并从 Gradle 问题中连接至该 issue。 github
最新版的 Android Gradle 插件版本为 4.1 (目前为 4.1.0-rc03),但若是您但愿获取全部的错误修复,请尝试最新的 4.2 版本 (目前为 4.2.0-alpha13)。Gradle 的版本应为 6.6,同时若是您正在使用 Kotlin,请将 Kotlin Gradle 插件更新为最新的 1.4 版 (相关 Kotlin issue)。最后使用如下代码更新 gradle.properties:web
org.gradle.unsafe.configuration-cache=true # 当心使用这一标记,由于有些插件尚未彻底兼容 org.gradle.unsafe.configuration-cache-problems=warn
查看全部 Android Gradle 插件版本,请参考以下页面:api
https://maven.google.com/web/index.html#com.android.tools.build:gradle缓存
若是启用了配置缓存,您应该能够在第一次运行时经过 Android Studio 的 Build 输出窗口或命令行看到 "Calculating task graph as no configuration cache is available for tasks…" (因为当前任务没有可用配置,正在生成任务图谱...) 字样;而在第二次运行中会复用配置缓存,因此输出中会包含 "Reusing configuration cache. (复用配置缓存)"。安全
不管您遇到任何问题,均可以在 Android Studio issue 跟踪 或 Gradle issue 跟踪 中向咱们反馈。服务器
想要深刻了解配置缓存,咱们要从了解构建的配置阶段开始。就算您开启了配置缓存,第一次构建仍会经历这一过程。在配置阶段,全部被包含的工程 (在评估 settings.gradle 时获取) 都会依据其构建文件的评估结果进行配置。一般首先会应用全部插件,同时 DSL 对象会被实例化;接下来会继续评估构建文件,而 DSL 对象将会被分配您所指定的值。当构建文件的评估完成时,会调用 Android Gradle 插件 (以及许多遵循相同模式的其余插件) 的 Project.afterEvaluate 回调。在此回调的调用期间,Android Gradle 插件会完成其绝大部分的工做,包括建立变体以及注册任务。
在评估 DSL 以及注册任务以后,接下来的阶段会构建一个任务图。您所要求执行的任务以及它们所依赖的任务都会被彻底配置。这一过程将会持续到触达没有依赖的叶子任务为止。配置的这一阶段将会输出一个任务图,Gradle 中的调度机制会使用该任务图来运行构建操做。当任务图被完成后,配置缓存会将其存储在磁盘中 (在 Gradle 6.6 中位于根工程的 .gradle/configuration-cache directory 目录下) 。它能够序列化全部的 Gradle-managed 类型 (如 FileCollection、Property、Provider) 以及全部用户定义的可序列化类型。在此阶段结束时,每一个任务的状态都将被彻底记录并保留下来。
在第二次构建时,假设 Gradle 可以复用记录的缓存,则会加载所请求任务的任务图、跳过 DSL 评估,任务配置等。这意味着全部任务都将被实例化,而它们的全部属性都将从缓存中加载。从这一时刻起,构建过程基本与无缓存构建无异,区别只是默认状况下能够并行运行任务以及复用缓存中的依赖项解析结果的优点。
为了保证正确性,Gradle 会持续跟踪会影响已缓存的任务图的全部输入,包括构建文件、请求执行的任务以及配置过程当中对于 Gradle 和系统属性的的访问。请求运行一组不一样的任务会产生一个不一样的任务图,因此须要建立一个新的缓存记录。一个须要使状态失效的例子是: 您修改了 build 文件或 buildSrc,并向环境变量或系统属性传递了一个不一样的值。为了检测这类变动,构建系统会建立一个缓存任务图时所使用的 build 文件的快照;此外,它还会检测 buildSrc 中是否有未更新的任务。最后,任何会影响配置阶段的值都应当被包装为 Gradle-managed 类型,这有助于构建系统对配置阶段中所使用的变量进行持续跟踪。
构建中应用的全部 Gradle 插件都必须与配置缓存兼容,Gradle 也所以引入了一组新的 API。下面是咱们对于配置缓存和新 API 所带来的约束进行的考察:
Gradle 插件中最多见的兼容性问题来自于在任务操做中使用 Task.getProject()。在使用配置缓存时,为了保持每一个任务彻底独立,任务将没法访问这一共享状态。因为 Project 实例能够访问 TaskContainer、ConfigurationContainer 以及其余在启用缓存的运行期间不会填充的对象,从而致使反映出无效的状态,因此禁用它是必须的。引入了不少可替代的 API,好比用于延迟对象建立的 ObjectFactory,还有能够用于获取项目文件系统分布状况的接口,好比 ProjectLayout,若是须要在构建中启动进程,可使用 ExecOperations。您能够参考 完整的 API 列表 来进行迁移工做。
若是您使用系统属性、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")) } }
在内部,Gradle 会对在配置阶段解析的值提供者 (value provider) 进行持续跟踪,每一个值提供者都会被视为一个构建逻辑输入。另外,除非调用 Provider.forUseAtConfigurationTime(),不然没法解析提供者,从而使得意外引入配置阶段输入的状况很难发生。如前文所述,任何 Gradle 会在 build 文件发生改变时使配置缓存失效,这一特性与 ProviderFactory API 一块儿确保了 Gradle 能够捕获影响任务图的全部内容。
若是您但愿能够在任务间共享一些工做,例如: 避免屡次链接到网络服务器或者避免屡次解析某些信息,那么可使用兼容配置缓存的 共享构建服务 来进行实现。就像任务同样,构建服务能够包含输入信息,而且这些内容会在第一次运行后序列化。缓存的运行将会简单地反序列化参数并实例化任务所需的构建服务。构建服务的额外好处是它与构建生命周期很是契合,若是您但愿在构建完成后释放一些资源,那么在您的构建服务中使用 AutoCloseable 即可以实现这一功能。因为没法被安全地序列化至磁盘,添加构建监听的操做与配置缓存不兼容。
在努力使 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 实例访问的其余对象。例如: 若是您的插件建立了配置,请将其做为 FileCollection 传递给任务。若是您须要构建目录位置,请将其记录在 task 的属性中:
旧代码
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> }
最后一条建议是,当您实现自定义可序列化类型时,要注意被序列化的内容。确保不要序列化派生属性,并让这些属性成为临时的或使用函数做为替代。举例来讲,在缓存运行时,您将会为 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 跟踪 向咱们报告您所遇到的任何问题。
编码愉快!