提高 Gradle 构建的性能
构建性能对于生产力至关重要。完成构建所需的时间越长,它们就越有可能扰乱您的开发流程。构建每天运行多次,因此即使很小的等待时间也会增加。持续集成 (CI) 构建也是如此:花费的时间越少,您对新问题的反应就越快,并且可以更频繁地进行试验。
所有这些意味着值得投入一些时间和精力来使您的构建尽可能快。本节提供了几种加快构建速度的方法。此外,您还将找到有关导致构建性能下降的原因以及如何避免这种情况的详细信息。
想要更快的 Gradle 构建吗?在此注册我们的构建缓存培训课程,了解 Develocity 如何将构建速度提高高达 90%。 |
检查你的构建
在进行任何更改之前,请使用构建扫描或配置文件报告检查您的构建。正确的构建检查可以帮助您了解:
-
构建您的项目需要多长时间
-
你的构建的哪些部分很慢
检查提供了一个比较点,可以更好地了解本页建议的更改的影响。
为了最好地利用此页面:
-
检查你的构建。
-
做出改变。
-
再次检查您的构建。
如果更改缩短了构建时间,请将其永久化。如果您没有看到改进,请删除更改并尝试其他更改。
更新版本
摇篮
Gradle 团队不断提高 Gradle 构建的性能。如果您使用旧版本的 Gradle,您就会错过这项工作的好处。跟上 Gradle 版本升级的风险很低,因为 Gradle 团队确保 Gradle 次要版本之间的向后兼容性。保持最新状态还可以更轻松地过渡到下一个主要版本,因为您会提前收到弃用警告。
Java
Gradle 在 Java 虚拟机 (JVM) 上运行。 Java 性能的改进通常有利于 Gradle。为了获得最佳 Gradle 性能,请使用最新版本的 Java。
插件
插件作者不断提高插件的性能。如果您使用旧版本的插件,您就会错过该工作的好处。 Android、Java 和 Kotlin 插件尤其可以显着影响构建性能。更新到这些插件的最新版本以提高性能。
启用并行执行
大多数项目都包含多个子项目。通常,其中一些子项目是相互独立的;也就是说,它们不共享状态。但默认情况下,Gradle 一次仅运行一项任务。要并行执行属于不同子项目的任务,请使用以下parallel
标志:
$ gradle <task> --parallel
要默认并行执行项目任务,请将以下设置添加到gradle.properties
项目根目录或 Gradle 主目录中的文件中:
org.gradle.parallel=true
并行构建可以显着缩短构建时间;多少取决于您的项目结构以及子项目之间有多少依赖关系。执行时间由单个子项目主导的构建根本不会受益太多。具有大量子项目间依赖关系的项目也不会。但大多数多子项目构建都会缩短构建时间。
通过构建扫描可视化并行性
构建扫描为您提供任务执行的可视化时间表。在以下示例构建中,您可以在构建的开始和结束时看到长时间运行的任务:
调整构建配置以尽早并行运行两个慢速任务,将总体构建时间从 8 秒减少到 5 秒:
重新启用 Gradle 守护进程
Gradle 守护进程通过以下方式减少构建时间:
-
跨构建缓存项目信息
-
在后台运行,因此每个 Gradle 构建都不必等待 JVM 启动
-
受益于 JVM 中的持续运行时优化
-
在运行构建之前观察文件系统以准确计算需要重建的内容
Gradle 默认启用守护进程,但某些构建会覆盖此首选项。如果您的构建禁用了守护进程,您可以看到启用守护进程后性能显着提高。
您可以在构建时使用以下daemon
标志启用守护进程:
$ gradle <task> --daemon
要在较旧的 Gradle 版本中默认启用守护程序,请将以下设置添加到
gradle.properties
项目根目录或 Gradle 主目录中的文件中:
org.gradle.daemon=true
在开发人员机器上,您应该会看到显着的性能改进。在 CI 机器上,长寿命代理受益于守护进程。但寿命短的机器并没有多大好处。在 Gradle 3.0 及更高版本中,守护进程会在内存压力时自动关闭,因此启用守护进程始终是安全的。
启用配置缓存
此功能有以下限制:
|
您可以通过启用配置缓存来缓存配置阶段的结果。当构建配置输入在构建之间保持相同时,配置缓存允许 Gradle 完全跳过配置阶段。
构建配置输入包括:
-
初始化脚本
-
设置脚本
-
构建脚本
-
配置阶段使用的系统属性
-
配置阶段使用的 Gradle 属性
-
配置阶段使用的环境变量
-
使用价值提供者(例如提供商)访问的配置文件
-
buildSrc
输入,包括构建配置输入和源文件
默认情况下,Gradle 不使用配置缓存。要在构建时启用配置缓存,请使用以下configuration-cache
标志:
$ gradle <task> --configuration-cache
要默认启用配置缓存,请将以下设置添加到gradle.properties
项目根目录或 Gradle 主目录中的文件中:
org.gradle.configuration-cache=true
有关配置缓存的更多信息,请查看 配置缓存文档。
为自定义任务启用增量构建
增量构建是一种 Gradle 优化,它会跳过之前使用相同输入执行的运行任务。如果任务的输入和输出自上次执行以来没有更改,Gradle 会跳过该任务。
Gradle 提供的大多数内置任务都支持增量构建。要使自定义任务与增量构建兼容,请指定输入和输出:
tasks.register("processTemplatesAdHoc") {
inputs.property("engine", TemplateEngineType.FREEMARKER)
inputs.files(fileTree("src/templates"))
.withPropertyName("sourceFiles")
.withPathSensitivity(PathSensitivity.RELATIVE)
inputs.property("templateData.name", "docs")
inputs.property("templateData.variables", mapOf("year" to "2013"))
outputs.dir(layout.buildDirectory.dir("genOutput2"))
.withPropertyName("outputDir")
doLast {
// Process the templates here
}
}
tasks.register('processTemplatesAdHoc') {
inputs.property('engine', TemplateEngineType.FREEMARKER)
inputs.files(fileTree('src/templates'))
.withPropertyName('sourceFiles')
.withPathSensitivity(PathSensitivity.RELATIVE)
inputs.property('templateData.name', 'docs')
inputs.property('templateData.variables', [year: '2013'])
outputs.dir(layout.buildDirectory.dir('genOutput2'))
.withPropertyName('outputDir')
doLast {
// Process the templates here
}
}
有关增量构建的更多信息,请查看 增量构建文档。
通过构建扫描时间线可视化增量构建
查看构建扫描时间线视图,以确定可以从增量构建中受益的任务。这还可以帮助您理解为什么当您希望 Gradle 跳过任务时任务会执行。
正如您在上面的构建扫描中看到的,该任务不是最新的,因为其输入之一(“时间戳”)发生了更改,迫使任务重新运行。
按持续时间对任务进行排序,以查找项目中最慢的任务。
启用构建缓存
构建缓存是一种 Gradle 优化,用于存储特定输入的任务输出。当您稍后使用相同的输入运行相同的任务时,Gradle 会从构建缓存中检索输出,而不是再次运行该任务。默认情况下,Gradle 不使用构建缓存。要在构建时启用构建缓存,请使用以下build-cache
标志:
$ gradle <task> --build-cache
要默认启用构建缓存,请将以下设置添加到gradle.properties
项目根目录或 Gradle 主目录中的文件中:
org.gradle.caching=true
您可以使用本地构建缓存来加速单台计算机上的重复构建。您还可以使用共享构建缓存来加速跨多台计算机的重复构建。发展提供了一种。共享构建缓存可以减少 CI 和开发人员构建的构建时间。
有关构建缓存的更多信息,请查看 构建缓存文档。
通过构建扫描可视化构建缓存
构建扫描可以帮助您调查构建缓存的有效性。在性能屏幕中,“构建缓存”选项卡显示有关以下内容的统计信息:
-
有多少任务与缓存交互
-
使用了哪个缓存
-
这些缓存条目的传输和打包/解包速率
“任务执行”选项卡显示有关任务可缓存性的详细信息。单击某个类别可查看突出显示该类别任务的时间线屏幕。
在时间线屏幕上按任务持续时间排序,以突出显示具有巨大节省时间潜力的任务。上面的构建扫描显示了可以改进:task1
并:task3
使其可缓存,并显示了 Gradle 不缓存它们的原因。
为特定开发人员工作流程创建构建
最快的任务是不执行的任务。如果您能找到跳过不需要运行的任务的方法,那么您最终将获得更快的整体构建速度。
如果您的构建包含多个子项目,请创建任务来独立构建这些子项目。这有助于您充分利用缓存,因为对一个子项目的更改不会强制重建不相关的子项目。这有助于减少处理不相关子项目的团队的构建时间:前端开发人员无需在每次更改前端时都构建后端子项目。即使文档与代码位于同一项目中,文档编写者也不需要构建前端或后端代码。
相反,创建符合开发人员需求的任务。您仍然拥有整个项目的单个任务图。每组用户建议对任务图进行限制视图:将该视图转换为排除不必要任务的 Gradle 工作流程。
Gradle 提供了几个功能来创建这些工作流程:
-
将任务分配给适当的组
-
创建聚合任务:没有任何操作、仅依赖于其他任务的任务,例如
assemble
-
通过和其他方式推迟配置
gradle.taskGraph.whenReady()
,因此您可以仅在必要时执行验证
增加堆大小
默认情况下,Gradle 为您的构建保留 512MB 的堆空间。这对于大多数项目来说已经足够了。但是,一些非常大的构建可能需要更多内存来保存 Gradle 的模型和缓存。如果您遇到这种情况,您可以指定更大的内存要求。在gradle.properties
项目根目录或 Gradle 主目录的文件中指定以下属性:
org.gradle.jvmargs=-Xmx2048M
要了解更多信息,请查看JVM 内存配置文档。
优化配置
如构建生命周期章节中所述,Gradle 构建经历 3 个阶段:初始化、配置和执行。无论运行什么任务,配置代码始终都会执行。因此,在配置期间执行的任何昂贵的工作都会减慢每次调用的速度。即使是简单的命令,如gradle help
和gradle tasks
。
接下来的几小节介绍了可以减少配置阶段所花费时间的技术。
您还可以启用配置缓存以减少缓慢配置阶段的影响。但即使使用缓存的机器仍然偶尔会执行您的配置阶段。因此,您应该使用这些技术尽可能快地完成配置阶段。 |
避免昂贵或阻塞的工作
您应该避免在配置阶段进行耗时的工作。但有时它可能会潜入您的构建中不明显的地方。如果该代码位于构建文件中,那么当您在配置期间加密数据或调用远程服务时,通常很清楚。但这样的逻辑更常见于插件和偶尔的自定义任务类中。插件apply()
方法或任务构造函数中任何昂贵的工作都是危险信号。
仅在需要的地方应用插件
应用于项目的每个插件和脚本都会增加总体配置时间。有些插件比其他插件有更大的影响。这并不意味着您应该避免使用插件,但您应该注意只在需要的地方应用它们。例如,即使不是每个项目都需要插件allprojects {}
,也可以轻松地将插件应用到所有子项目。subprojects {}
在上面的构建扫描示例中,您可以看到根构建脚本将该script-a.gradle
脚本应用于构建内的 3 个子项目:
该脚本运行需要 1 秒。由于它适用于 3 个子项目,因此该脚本累计延迟了配置阶段 3 秒。在这种情况下,有几种方法可以减少延迟:
-
如果只有一个子项目使用该脚本,您可以从其他子项目中删除该脚本应用程序。这将每次 Gradle 调用中的配置延迟减少了两秒。
-
如果多个子项目(但不是全部)使用该脚本,您可以将脚本和所有周围逻辑重构为位于
buildSrc
.仅将自定义插件应用于相关子项目,减少配置延迟并避免代码重复。
静态编译任务和插件
插件和任务作者经常编写 Groovy,因为它具有简洁的语法、JDK 的 API 扩展以及使用闭包的函数方法。但 Groovy 语法伴随着动态解释的成本。因此,Groovy 中的方法调用比 Java 或 Kotlin 中的方法调用花费更多时间并使用更多 CPU。
您可以通过静态 Groovy 编译来降低此成本:@CompileStatic
当您不明确需要动态功能时,将注释添加到 Groovy 类中。如果您需要在方法中使用动态 Groovy,请将@CompileDynamic
注释添加到该方法。
或者,您可以使用静态编译语言(例如 Java 或 Kotlin)编写插件和任务。
警告: Gradle 的 Groovy DSL 严重依赖 Groovy 的动态功能。要在插件中使用静态编译,请切换到类似 Java 的语法。
以下示例定义了一个不带动态功能的复制文件的任务:
project.tasks.register('copyFiles', Copy) { Task t ->
t.into(project.layout.buildDirectory.dir('output'))
t.from(project.configurations.getByName('compile'))
}
此示例使用所有 Gradle“域对象容器”上可用的register()
和方法。getByName()
域对象容器包括任务、配置、依赖项、扩展等等。某些集合(例如TaskContainer
)具有专用类型和额外的方法,例如create,它接受任务类型。
当您使用静态编译时,IDE 可以:
-
快速显示与无法识别的类型、属性和方法相关的错误
-
自动完成方法名称
优化依赖解析
依赖项解析简化了将第三方库和其他依赖项集成到项目中的过程。 Gradle 联系远程服务器来发现和下载依赖项。您可以优化引用依赖项的方式来减少这些远程服务器调用。
避免不必要和未使用的依赖项
管理第三方库及其传递依赖项会显着增加项目维护成本和构建时间。
注意未使用的依赖项:当第三方库停止使用时,不会从依赖项列表中删除。这种情况在重构过程中经常发生。您可以使用Gradle Lint 插件 来识别未使用的依赖项。
如果您只使用第三方库中的少量方法或类,请考虑:
-
在您的项目中自己实现所需的代码
-
如果它是开源的,则从库中复制所需的代码(带有归属!)
优化存储库顺序
当 Gradle 解析依赖项时,它会按声明的顺序搜索每个存储库。为了减少搜索依赖项所花费的时间,请首先声明托管最大数量依赖项的存储库。这最大限度地减少了解决所有依赖关系所需的网络请求数量。
最小化动态和快照版本
动态版本(例如“2.+”)和更改版本(快照)迫使 Gradle 联系远程存储库以查找新版本。默认情况下,Gradle 仅每 24 小时检查一次。但您可以通过以下设置以编程方式更改此设置:
-
cacheDynamicVersionsFor
-
cacheChangingModulesFor
如果构建文件或初始化脚本降低了这些值,Gradle 会更频繁地查询存储库。如果您不需要每次构建时都使用依赖项的绝对最新版本,请考虑删除这些设置的自定义值。
通过构建扫描查找动态和变化的版本
您可以通过构建扫描找到具有动态版本的所有依赖项:
您也许可以使用“1.2”和“3.0.3.GA”等允许 Gradle 缓存版本的固定版本。如果您必须使用动态且不断变化的版本,请调整缓存设置以最好地满足您的需求。
在配置过程中避免依赖解析
无论是在 I/O 方面还是在计算方面,依赖性解析都是一个昂贵的过程。 Gradle 通过缓存减少所需的网络流量。但仍然有成本。 Gradle 在每次构建时都会运行配置阶段。如果您在配置阶段触发依赖项解析,则每个构建都会付出该成本。
切换到声明式语法
如果您评估配置文件,您的项目将在配置期间支付依赖项解析的成本。通常,任务会评估这些文件,因为在您准备好在任务操作中使用这些文件之前,您不需要这些文件。想象一下您正在进行一些调试并想要显示构成配置的文件。要实现这一点,您可以注入一条 print 语句:
tasks.register<Copy>("copyFiles") {
println(">> Compilation deps: ${configurations.compileClasspath.get().files.map { it.name }}")
into(layout.buildDirectory.dir("output"))
from(configurations.compileClasspath)
}
tasks.register('copyFiles', Copy) {
println ">> Compilation deps: ${configurations.compileClasspath.files.name}"
into(layout.buildDirectory.dir('output'))
from(configurations.compileClasspath)
}
该files
属性强制 Gradle 解决依赖关系。在此示例中,这发生在配置阶段。由于配置阶段在每个构建上运行,因此所有构建现在都会付出依赖性解析的性能成本。您可以通过以下doFirst()
操作来避免此成本:
tasks.register<Copy>("copyFiles") {
into(layout.buildDirectory.dir("output"))
// Store the configuration into a variable because referencing the project from the task action
// is not compatible with the configuration cache.
val compileClasspath: FileCollection = configurations.compileClasspath.get()
from(compileClasspath)
doFirst {
println(">> Compilation deps: ${compileClasspath.files.map { it.name }}")
}
}
tasks.register('copyFiles', Copy) {
into(layout.buildDirectory.dir('output'))
// Store the configuration into a variable because referencing the project from the task action
// is not compatible with the configuration cache.
FileCollection compileClasspath = configurations.compileClasspath
from(compileClasspath)
doFirst {
println ">> Compilation deps: ${compileClasspath.files.name}"
}
}
请注意,该from()
声明不会解析依赖项,因为您使用依赖项配置本身作为参数,而不是文件。任务Copy
在任务执行期间自行解析配置。
通过构建扫描可视化依赖关系解析
构建扫描的性能页面上的“依赖项解析”选项卡显示配置和执行阶段的依赖项解析时间:
构建扫描提供了另一种识别此问题的方法。您的构建应该在“项目配置”期间花费 0 秒来解决依赖关系。此示例显示构建在生命周期中过早地解决了依赖关系。您还可以在“性能”页面上找到“设置和建议”选项卡。这显示了在配置阶段解决的依赖关系。
删除或改进自定义依赖解析逻辑
Gradle 允许用户以最适合他们的方式对依赖解析进行建模。简单的自定义(例如强制依赖项的特定版本或将一种依赖项替换为另一种依赖项)不会对依赖项解析时间产生太大影响。更复杂的自定义(例如下载和解析 POM 的自定义逻辑)可能会显着减慢依赖关系解析速度。
使用构建扫描或配置文件报告来检查自定义依赖项解析逻辑是否不会对依赖项解析时间产生不利影响。这可能是您自己编写的自定义逻辑,也可能是插件的一部分。
删除缓慢或意外的依赖项下载
缓慢的依赖项下载可能会影响您的整体构建性能。有多种原因可能会导致这种情况,包括互联网连接速度慢或存储库服务器过载。在构建扫描的“性能”页面上,您将找到“网络活动”选项卡。此选项卡列出的信息包括:
-
下载依赖项所花费的时间
-
依赖下载的传输速率
-
按下载时间排序的下载列表
在以下示例中,两次缓慢的依赖项下载分别花费了 20 秒和 40 秒,并降低了构建的整体性能:
检查下载列表是否有意外的依赖项下载。例如,您可能会看到由使用动态版本的依赖项引起的下载。
通过切换到不同的存储库或依赖项来消除这些缓慢或意外的下载。
优化 Java 项目
以下部分仅适用于使用java
插件或其他 JVM 语言的项目。
优化测试
项目经常花费大量的构建时间进行测试。这些可以是单元测试和集成测试的混合。集成测试通常需要更长的时间。构建扫描可以帮助您识别最慢的测试。然后您可以专注于加快这些测试的速度。
上面的构建扫描显示了运行测试的所有项目的交互式测试报告。
Gradle 有多种方法来加速测试:
-
并行执行测试
-
将测试分叉到多个进程中
-
禁用报告
让我们依次看看其中的每一个。
并行执行测试
Gradle 可以并行运行多个测试用例。要启用此功能,请覆盖maxParallelForks
相关任务上的值Test
。为了获得最佳性能,请使用小于或等于可用 CPU 核心数量的数字:
tasks.withType<Test>().configureEach {
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
}
tasks.withType(Test).configureEach {
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
}
并行测试必须是独立的。他们不应共享文件或数据库等资源。如果您的测试确实共享资源,它们可能会以随机且不可预测的方式相互干扰。
将测试分叉到多个进程中
默认情况下,Gradle 在单个分叉虚拟机中运行所有测试。如果有很多测试,或者某些测试消耗大量内存,则您的测试可能需要比您预期的运行时间更长的时间。您可以增加堆大小,但垃圾收集可能会减慢您的测试速度。
或者,您可以在使用以下设置运行一定数量的测试后分叉一个新的测试虚拟机forkEvery
:
tasks.withType<Test>().configureEach {
forkEvery = 100
}
tasks.withType(Test).configureEach {
forkEvery = 100
}
分叉虚拟机是一项昂贵的操作。此处设置太小的值会减慢测试速度。 |
禁用报告
无论您是否想查看测试报告,Gradle 都会自动创建它们。该报告的生成会减慢整体构建速度。如果出现以下情况,您可能不需要报告:
-
你只关心测试是否成功(而不是为什么)
-
您使用构建扫描,它提供比本地报告更多的信息
要禁用测试报告,请在任务中设置reports.html.required
和:reports.junitXml.required
false
Test
tasks.withType<Test>().configureEach {
reports.html.required = false
reports.junitXml.required = false
}
tasks.withType(Test).configureEach {
reports.html.required = false
reports.junitXml.required = false
}
有条件地启用报告
您可能希望有条件地启用报告,这样您就不必编辑构建文件来查看它们。要启用基于项目属性的报告,请在禁用报告之前检查属性是否存在:
tasks.withType<Test>().configureEach {
if (!project.hasProperty("createReports")) {
reports.html.required = false
reports.junitXml.required = false
}
}
tasks.withType(Test).configureEach {
if (!project.hasProperty("createReports")) {
reports.html.required = false
reports.junitXml.required = false
}
}
然后,在命令行上传递该属性-PcreateReports
以生成报告。
$ gradle <task> -PcreateReports
gradle.properties
或者在项目根目录或 Gradle 主目录中的文件中配置属性:
createReports=true
优化编译器
Java 编译器速度很快。但是,如果您要编译数百个 Java 类,那么即使很短的编译时间也会增加。 Gradle 为 Java 编译提供了多种优化:
-
将编译器作为单独的进程运行
-
将仅限内部的依赖关系切换为实现可见性
将编译器作为单独的进程运行
JavaCompile
您可以将编译器作为单独的进程运行,并为任何任务使用以下配置:
<task>.options.isFork = true
<task>.options.fork = true
要将配置应用到所有Java编译任务,您可以在configureEach
java编译任务中:
tasks.withType<JavaCompile>().configureEach {
options.isFork = true
}
tasks.withType(JavaCompile).configureEach {
options.fork = true
}
Gradle 在构建期间重用此过程,因此分叉开销很小。通过将内存密集型编译分叉到一个单独的进程中,我们可以最大限度地减少主 Gradle 进程中的垃圾收集。更少的垃圾收集意味着 Gradle 的基础设施可以运行得更快,特别是当您还使用并行构建时。
分叉编译很少影响小项目的性能。但是,如果单个任务将一千多个源文件一起编译,则应该考虑它。
将仅限内部的依赖关系切换为实现可见性
只有库可以定义api 依赖关系。使用该
java-library 插件定义库中的 API 依赖项。使用该插件的项目java 无法声明api 依赖项。
|
在 Gradle 3.4 之前,项目使用compile
配置声明依赖项。这将所有这些依赖关系暴露给下游项目。在 Gradle 3.4 及更高版本中,您可以将面向下游的api
依赖项与仅限内部的implementation
详细信息分开。实现依赖项不会泄漏到下游项目的编译类路径中。当实现细节发生变化时,Gradle 仅重新编译api
依赖项。
dependencies {
api(project("my-utils"))
implementation("com.google.guava:guava:21.0")
}
dependencies {
api project('my-utils')
implementation 'com.google.guava:guava:21.0'
}
这可以显着减少由大型多项目构建中的单个更改引起的重新编译的“连锁反应”。
提高旧版 Gradle 版本的性能
有些项目无法轻松升级到当前的 Gradle 版本。虽然您应该尽可能将 Gradle 升级到最新版本,但我们认识到这对于某些特定情况并不总是可行。在这些特定情况下,请查看这些建议来优化旧版本的 Gradle。
启用守护进程
Gradle 3.0及以上版本默认启用Daemon。如果您使用的是旧版本,您应该更新到最新版本的 Gradle。如果您无法更新 Gradle 版本,您可以手动启用 Daemon。
使用增量编译
Gradle 可以分析依赖关系到单个类级别,以便仅重新编译受更改影响的类。 Gradle 4.10及以上版本默认启用增量编译。要在较旧的 Gradle 版本中默认启用增量编译,请将以下设置添加到您的
build.gradle
文件中:
tasks.withType<JavaCompile>().configureEach {
options.isIncremental = true
}
tasks.withType(JavaCompile).configureEach {
options.incremental = true
}
优化Android项目
此页面上的所有内容都适用于 Android 构建,因为 Android 构建使用 Gradle。然而,Android 引入了独特的优化机会。有关更多信息,请查看 Android 团队绩效指南。您还可以观看 Google IO 2017 的附带演讲。