本文罗列了目前社区中常规的 gradle 构建速度优化方案,作为开发的参考。
更新于:2020-2-26

gradle配置

1. daemon

无需每次执行构建相关任务时都重新启动一个新的gradle进程,从而增加了相应的非必要耗时。

1
2
3
4
// 在项目的gradle.properties 增加 
org.gradle.daemon=true
// 或通过命令行传递参数
./gradlew --daemon

如果在长时间多次编译后速度变得缓慢,可以通过 ./gradlew --stop 结束已有daemon进程。

2. parallel

基本上大部分项目都包含了多个module,gradle默认情况下是串行执行的,可以通过以下方式开启并行构建,可以加入参数 –profile 在生成的html中看到total build time 和 task execution的时间差异。

1
2
3
4
// 在项目的gradle.properties 增加 
org.gradle.parallel=true
// 或通过命令行传递参数
./gradlew --parallel

3. configure on demand

gradle的构建包含初始化、配置、执行三大流程,CoD用于配置阶段,仅配置与所选任务相关的module。
例如module-A compile module-B,则B会被配置;或者task-A dependOn module-B‘s task 则B也会被配置。

但是我们大部分情况是在IDE中执行的 sync 触发了配置所有module,因此CoD并没起什么作用。Gradle团队也决定废弃它,并计划通过一个新的模式来替代:issue

1
2
3
4
// 在项目的gradle.properties 增加 
org.gradle.configureondemand=true
// 或通过命令行传递参数
./gradlew --configure-on-demand

4. offline

每次构建都会去下载本地缓存没有的依赖或检查缓存已有依赖是否存在最新版本,另外一些三方插件在构建期间也会发起网络请求,例如一些会上传匿名统计信息、一些会上传混淆的mapping文件等等。但是由于国内糟糕的网络环境,网络经常超时重试占用大量构建时间。

1
2
3
// 1. 在IDE中的gradle panel里 toggle offline mode
// 2. 或通过命令行传递参数
./gradlew --offline

5. buildCache

gradle的构建缓存机制 会缓存构建的输出(build-in 和 符合的三方task),并以输入作为参数生成唯一的key生成对应的缓存文件,这样在后续的构建过程中如果输入没有变化就可以直接利用这些缓存加快构建速度。
特别是clean之后重新构建时,实测有无缓存的时间相差将近 2/3

1
2
3
4
5
6
// 在项目的gradle.properties 增加 
org.gradle.caching=true
// 或通过命令行传递参数
./gradlew --build-cache
// 如果在开启了caching,想临时测试关闭构建缓存的执行效果
./gradlew --no-build-cache

生成的缓存默认存储在 <user-home>/.android/build-cache/ 可以通过以下方式进行修改或删除:

1
2
3
4
5
6
7
8
9
10
// 1. 修改位置:
// 在setting.gradle中增加
buildCache {
local {
directory = new File(rootDir, 'build-cache')
removeUnusedEntriesAfterDays = 30
}
}
// 2. 删除构建缓存:
./gradlew cleanBuildCache

与增量编译(incremental build)机制一个很明显的区别就是在构建的输出日志中,增量编译的task之后显示的是 UP-TO-DATE ,而构建缓存显示的是 FROM-CACHE

6. jvmargs

由于构建所处的进程也是一个JVM,因此通常Java开发对JVM的调优参数也适用此构建进程,可以根据自身电脑的配置传递合适的jvm参数:

1
2
// 在项目的gradle.properties 增加 
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

AGP配置

1. minSdkVersion 21

64K问题导致针对5.0以下的设备需要通过multiDex方案来兼容,构建期间在拆分dex的同时还需要 花费一些时间 来决定哪些class需要放入primary dex以避免启动发生Crash。而从5.0开始虽然构建期间依然会拆分dex,但由于应用在安装时这些dex会被优化为一个 .oat 文件,也就无需再区分primary dex。
因此如果当前调试的设备版本>=5.0,完全可以仅在debug下将 minSdkVersion 指定为 21 来避免前述的耗时。

2. resConfigs

除非是针对不同语言或特定设备显示效果的特定测试,一般情况下我们开发环境仅包含一套资源即可,尽量减少aapt的编译耗时。

1
2
// 在app.gradle中的 defaultConfig 或特定 flavor 中
resConfigs "en", "xxhdpi"

3. crunchPngs

在构建期间 aapt 会自动对 png 图片进行压缩处理,但这在开发环境下一般是没必要的,因此可以停用。

1
2
3
4
5
6
// 在AGP3.0.0以下
aaptOptions {
cruncherEnabled false
}
// 从AGP3.0.0开始,buildType为debug时默认已经关闭,如果有自定义type,可增加如下
crunchPngs false

4. 静态化

我们可能经常会将一些动态信息通过 app.gradle 配置写入到 manifest.xml 或 BuildConfig.java 文件中,例如绑定编译时间、git提交版本等,但是这些属性往往会触发全量构建,即使每次只是修改了一行项目内的代码。
因此在开发环境尽量避免这些行为,可以通过判断是否release来分别配置。

Kotlin配置

现在很多项目都接入了Kotlin享受其带来的开发收益,但其编译速度却大受诟病,不过随着语言的迭代其编译速度也一直在优化。

from 1.2.21

从该版本开始,Kotlin的编译任务也支持了 buildCache 特性,所以在开启buildCache 的前提下直接 升级到最新版本 就能享受到期带来的收益。
另外即使开启了buildCache,注解处理器 kapt 的缓存默认也是禁用的,因此需要手动开启:

1
2
3
kapt {
useBuildCache = true
}

from 1.2.60

从该版本开始,对kapt利用gradle的work api实现并行任务处理进行了优化,开启方式如下:

1
2
// 在gradle.properties 中
kapt.use.work.api=true

from 1.3.20

从该版本开始,针对kapt引用了 Compile Avoidance 特性来避免一些情况下的注解处理任务,具体规则链接。但前提是需要注解处理器显式地声明使用kapt处理,因此通过以下方式来禁止在编译路径下自动搜索注解处理器:

1
2
// 在gradle.properties 中
kapt.include.compile.classpath=false

from 1.3.30

从该版本开始,kapt支持增量注解处理,通过以下方式开启:

1
2
// 在gradle.properties 中
kapt.incremental.apt=true

截止目前(1.3.61),以上特性由于还处于实验阶段,因此需要手动开启,不过随着版本的更新优化还在继续,也许在今后的某些版本中就会修改为默认开启。

其它实践

1. 版本问题

对于AS 和 AGP,除了新需求外新版本相对老版本一般都有性能方面的优化。

  • 例如老版的AS对于包含多个buildType和flavor配置的项目,每次sync都会将所有组合同步一遍(可以在build目录中看到所有组合的相关目录),非常耗时,而从AS3.3 + AGP3.3.0开始,在gradle文件发生变化时默认仅sync当前指定的flavor。
  • 又如AGP 3.5.0 和AS 3.5.0 实施了 Project Marble 计划,主要针对的就是对构建速度的优化。

因此在没有严重BUG的情况下,保持开发环境为最新版本能得到最新的性能优化。

2. 精简module

每增加一个module(模块)都会引入对应的 初始化 和 配置 tasks,大量的模块会对整个构建时间产生不可忽略的影响。可以挨个审查项目中的模块,能移除的尽量移除;对于基本上不会变动的模块,可以考虑打包为aar形式的依赖,AGP的构建缓存机制可以对其进行缓存以提升构建速度。

3. 三方库

Firebase Crashlytics (Fabric)

Crashlytics是用于收集bug的sdk,在国内使用也相当普遍,但是以下注意点会对构建速度产生影响:
crashlytics 会在每次构建期间构造一个ID来标识唯一性,但该ID是存储在manifest中所以会导致全量构建,可以在 buildType 中使用 ext.alwaysUpdateBuildId = false 在开发期间关闭。

4. command-line

建议尽量通过命令行来执行构建任务。通过IDE形式开启构建会将任务的执行进度、状态以及日志等信息与UI进行同步,根据电脑配置的不同该方案对构建性能有一定的影响。

5. 其它方案

  • freeline:当年前东家的项目中集成使用过,在生效的基础上单从速度而言确实比当时的instant run快,可以缓解部分情况下的问题。但是由于增量条件严苛(例如需要建立app进程和电脑socket通信所以首次插上时得全量编译、资源变化也会降级为全量等等),再加上仍然存在BUG(例如显示生效了但app中并没有变化,此时只能全量重编)实际上不适合作为常规开发解决方案。
  • fastdex

虽然以上两种方式由于AGP的升级和一些其它限制导致事实上不再适用现在大部分项目,但其优化方案是值得学习和借鉴的。

性能分析

--profile 可以获取当次构建的结果报告,包含所有task的具体执行时间,可以以此分析可优化的点;生成的文件一般位于项目根路径下的 reports 目录下。
--info 可以获得更详细的构建输出日志,例如其中就包含了一些task为什么被执行的原因。
>> <file path> 构建过程的日志非常多,难以详细定位,可以通过该方式将日志输出到指定的文件中。

针对某一项的优化措施需要排除其它因素干扰,可以通过以下方式获取全量构建:

1
2
3
./gradlew clean
./gradlew --stop
./gradlew --profile --offline --no-build-cache --rerun-tasks assembleXXX

其中 --rerun-tasks 强制重新执行所有相关task并忽略任何task优化。