什么是配置缓存?
配置缓存是一个晋升 IDE 和命令行构建速度的根底构建块。这是 Gradle 6.6 版本提供的一个高度试验性功能,它能够使构建零碎记录一次工作的图谱信息,并在接下来的构建中进行复用,从而防止再一次配置整个工程。这一性能也是配置阶段改良的连续,这些改良中引入了 惰性配置 (lazy configuration),以防止在构建的配置阶段进行不必要的工作。这些改良对于疾速迭代开发的重要性不言自明,而后者也是 Android Studio 团队所继续关注的一个用例。
性能改良
这一性能的次要指标便是晋升构建速度。在 Android 版 Santa Tracker 工程的基准化剖析中,对于启用了配置缓存的构建过程,咱们测量出其在 Android Studio 中的总构建工夫缩小了 35% (从 688ms 到 443ms,测试平台为 Linux,应用 Intel® Xeon® Gold 6154 CPU @ 3.00GHz)。下图展现了应用和不应用配置缓存进行 100 次构建的均匀总构建工夫 (以毫秒为单位):
对于一些工程,配置阶段可能会耗费 10 秒钟以上,节省时间的成果也因而更加显著。无论运行的是全新构建、增量构建还是更新构建,配置阶段的开销都是雷同的。要掂量您的构建过程中配置阶段所耗费的工夫,能够以空运行模式 (dry run mode) 运行工作,例如: ./gradlew :app:assembleDebug --dry-run
。
为了进一步防止反复运行配置过程,配置缓存还容许来自同一工程的工作并行运行。以前,只有利用 Worker API 的工作能够同时运行,然而因为配置缓存能够确保工作独立且无法访问全局共享状态 (例如 Project 实例),因而能够默认启用此行为。而且,依赖关系解析后果能够在运行间进行缓存,从而有助于优化整体构建工夫。
如何试用?
配置缓存性能当初还处于试验阶段,咱们心愿您能够尝试它并向咱们提供反馈。为了在您的构建中应用它,须要保障所有工程所利用的所有插件都是兼容的,这是为了平安地 (反) 序列化工作图。您可能须要更新某些 Gradle 插件。您能够通过此 issue 来获取受反对插件的残缺列表,如果您应用的插件不在其中,请在它们的问题跟踪器中提交问题,并从 Gradle 问题中链接至该 issue。
最新版的 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:
org.gradle.unsafe.configuration-cache=true
# 小心应用这一标记,因为有些插件还没有齐全兼容
org.gradle.unsafe.configuration-cache-problems=warn
查看所有 Android Gradle 插件版本,请参考如下页面:
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 API
构建中利用的所有 Gradle 插件都必须与配置缓存兼容,Gradle 也因而引入了一组新的 API。上面是咱们对于配置缓存和新 API 所带来的束缚进行的考查:
在工作中应用 Project 实例
Gradle 插件中最常见的兼容性问题来自于在工作操作中应用 Task.getProject()。在应用配置缓存时,为了放弃每个工作齐全独立,工作将无法访问这一共享状态。因为 Project 实例能够拜访 TaskContainer、ConfigurationContainer 以及其余在启用缓存的运行期间不会填充的对象,从而导致反映出有效的状态,所以禁用它是必须的。引入了很多可代替的 API,比方用于提早对象创立的 ObjectFactory,还有能够用于获取我的项目文件系统散布状况的接口,比方 ProjectLayout,如果须要在构建中启动过程,能够应用 ExecOperations。您能够参考 残缺的 API 列表 来进行迁徙工作。
拜访 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"))
}
}
在外部,Gradle 会对在配置阶段解析的值提供者 (value provider) 进行继续跟踪,每个值提供者都会被视为一个构建逻辑输出。另外,除非调用 Provider.forUseAtConfigurationTime(),否则无奈解析提供者,从而使得意外引入配置阶段输出的状况很难产生。如前文所述,任何 Gradle 会在 build 文件产生扭转时使配置缓存生效,这一个性与 ProviderFactory API 一起确保了 Gradle 能够捕捉影响工作图的所有内容。
在工作间共享工作
如果您心愿能够在工作间共享一些工作,例如: 防止屡次连贯到网络服务器或者防止屡次解析某些信息,那么能够应用兼容配置缓存的 共享构建服务 来进行实现。就像工作一样,构建服务能够蕴含输出信息,并且这些内容会在第一次运行后序列化。缓存的运行将会简略地反序列化参数并实例化工作所需的构建服务。构建服务的额定益处是它与构建生命周期十分符合,如果您心愿在构建实现后开释一些资源,那么在您的构建服务中应用 AutoCloseable 便能够实现这一性能。因为无奈被平安地序列化至磁盘,增加构建监听的操作与配置缓存不兼容。
从迁徙 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 实例拜访的其余对象。例如: 如果您的插件创立了配置,请将其作为 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 跟踪 向咱们报告您所遇到的任何问题。
编码欢快!