跳到主要内容

Gradle Task Lazy Configuration

Gradle build 耗时长是个老问题了,项目越大问题越严重,Android 开发团队也一直在想办法优化。本文介绍的 Task Lazy Configuration 也是最新的优化手段之一。

引子

在 Gradle 脚本中如下遍历 build variant 的依赖 classpath:

android.libraryVariants.each { variant ->
if (variant.name.endsWith("release")) {
androidJavadoc {
classpath += variant.javaCompile.classpath
}
}
}

这里的variant.javaCompile调用会引发如下的警告:

INFO: API 'variant.getJavaCompile()' is obsolete and has been replaced with 'variant.getJavaCompileProvider()'. It will be removed at the end of 2019. For more information, see https://d.android.com/r/tools/task-configuration-avoidance. To determine what is calling variant.getJavaCompile(), use -Pandroid.debug.obsoleteApi=true on the command line to display more information. Affected Modules: library

这个警告,大家在写 Gradle 脚本时可能都见过。现在已经2020年了,也就是说 Gradle 官方已经只支持新的方式了,那么,这到底意味着什么?我们又需要做什么呢?

警告来源

Android 中引入这个改动的是 Android Gradle plugin 3.3.0 (2019年1月),此版本插件对应的 Gradle 版本升级到了4.10.1+

此版本的 Behavior changes 中有如下一条:

Lazy task configuration: The plugin now uses Gradle’s new task creation API to avoid initializing and configuring tasks that are not required to complete the current build (or tasks not on the execution task graph). For example, if you have multiple build variants, such as “release” and “debug” build variants, and you're building the “debug” version of your app, the plugin avoids initializing and configuring tasks for the “release” version of your app.

Calling certain older methods in the Variants API, such as variant.getJavaCompile(), might still force task configuration. To make sure that your build is optimized for lazy task configuration, invoke new methods that instead return a TaskProvider object, such as variant.getJavaCompileProvider().

If you execute custom build tasks, learn how to adapt to Gradle’s new task-creation API.

由此我们了解到在Gradle 4.9版本中,引入了新的 Task Configuration Avoidance API

Task Configuration Avoidance API

In a nutshell, the API allows builds to avoid the cost of creating and configuring tasks during Gradle’s configuration phase when those tasks will never be executed. For example, when running a compile task, other unrelated tasks, like code quality, testing and publishing tasks, will not be executed, so any time spent creating and configuring those tasks is unnecessary. The configuration avoidance API avoids configuring tasks if they will not be needed during the course of a build, which can have a significant impact on total configuration time.

简单来说,就是只 configure 需要 execute 的(及其依赖的)Task,减少 build 过程中 configuration 时间。

关于 configuration 和 execution,都是属于 Gradle Build Lifecycle,我们简单来了解一下。

Gradle Build Lifecycle

A Gradle build has three distinct phases.

Initialization Gradle supports single and multi-project builds. During the initialization phase, Gradle determines which projects are going to take part in the build, and creates a Project instance for each of these projects.

Configuration During this phase the project objects are configured. The build scripts of all projects which are part of the build are executed.

Execution Gradle determines the subset of the tasks, created and configured during the configuration phase, to be executed. The subset is determined by the task name arguments passed to the gradle command and the current directory. Gradle then executes each of the selected tasks.

示例

让我们来看具体的例子:

首先,Android 默认有2个 build type: debug/release,我们再声明2个 product flavor:

flavorDimensions "version"
productFlavors {
demo {
dimension "version"
}
full {
dimension "version"
}
}

这样最终会产生 2 * 2 = 4 个 build variant 如下:

  • demoDebug
  • demoRelease
  • fullDebug
  • fullRelease

然后我们再仿照Android Gradle plugin的做法,为每个 build variant 生成一个名为play的 Task:

android.applicationVariants.all { variant ->
// 旧API - 非lazy configuration
task("play${variant.name.capitalize()}") {
println "$name start"
sleep(2000) // 休眠2秒,模拟耗时操作
println "$name stop"
}
}

这样我们就创建了如下4个 Task:

  • playDemoDebug
  • playDemoRelease
  • playFullDebug
  • playFullRelease

注意,创建 Task 时我们使用的是旧的 API,也就是非"lazy"的,我们 execute 其中的任意1个时,所有4个都会被 configure。 运行验证一下:

./gradlew playDemoDebug

> Configure project :app
playDemoDebug start
playDemoDebug stop
playDemoRelease start
playDemoRelease stop
playFullDebug start
playFullDebug stop
playFullRelease start
playFullRelease stop

BUILD SUCCESSFUL in 8s

可以看到,在 Configuration 阶段,4个 Task 的 configure 逻辑都被执行了。 为了让结果更明显,我们为 configure 设置了2秒的延时,而其他环节没有执行任何操作,耗时几乎为0。 build 的总耗时8秒,约等于单次 Task configuration 的耗时(2秒) * Task 数量(4),符合预期。

然后我们改用新的 API 来创建 Task:

android.applicationVariants.all { variant ->
// 新API - lazy configuration
tasks.register("play${variant.name.capitalize()}") {
println "$name start"
sleep(2000)
println "$name stop"
}
}

此时 Task 的 configuration 应该是"lazy"的,也就是说只有被 execute 的 Task 才会被 configure。 再次运行验证一下:

./gradlew playDemoDebug
playDemoDebug start
playDemoDebug stop

BUILD SUCCESSFUL in 2s

可以看到,确实只 configure 了被 execute 的 playDemoDebug Task,build 的总耗时也相应下降到了约为单次 Task configuration 耗时的2秒,符合预期。

意义

可以明显看到,在 build variant 较多且 configuration 耗时较长的情况下,这个优化会极大的减少 build 单个 variant 所需要的时间。

结论

  1. 保持 AGP/Gradle 的更新,对开发效率提升巨大。
  2. 如果自己编写 Gradle 脚本或插件时创建了 task ,应该尽快迁移使用新的API。