12、构建性能优化:构建速度分析(--profile/Build Scan)、并行构建(parallel)、守护进程(daemon)调优、避免反模式
说实话,Android 项目越做越大,最让人头疼的就是构建速度。我见过不少团队,改一行代码要等三分钟才能看到效果——这谁受得了?
这一章,我就把自己这些年优化构建速度的经验,掰开了揉碎了讲给你听。咱们从「怎么找到瓶颈」开始,再到「怎么调参数」,最后聊聊「哪些坑千万别踩」。
12.1 构建速度分析:先诊断,再开药
优化之前,你得先知道慢在哪。我见过有人上来就改配置,结果改了半天,发现瓶颈在别的地方。这就很尴尬了。
12.1.1 使用 --profile 生成构建报告
Gradle 自带了一个性能分析工具,就是 --profile 参数。用法很简单:
./gradlew assembleDebug --profile
跑完之后,在 build/reports/profile/ 目录下会生成一个 HTML 报告。里面会告诉你:
- 每个 Task 花了多少时间
- 配置阶段、依赖解析、任务执行分别用了多久
- 哪些 Task 是串行的,哪些是并行的
我个人习惯先看「Task Execution」那一栏。如果某个 Task 耗时特别长,比如 mergeDebugResources 占了 40% 的时间,那这就是你要优化的重点。
12.1.2 使用 Build Scan 做更深入的分析
如果你觉得 --profile 的信息不够直观,可以试试 Build Scan。它是 Gradle 官方提供的云端分析工具。
启用方式很简单,在 build.gradle 里加上:
// settings.gradle
plugins {
id 'com.gradle.build-scan' version '3.16.1'
}
buildScan {
termsOfServiceUrl = 'https://gradle.com/terms-of-service'
termsOfServiceAgree = 'yes'
}
然后正常跑构建:
./gradlew assembleDebug --scan
构建结束后,终端会输出一个 URL。打开它,你会看到一个非常详细的报告:
- 每个 Task 的耗时,按时间轴排列
- 依赖下载的耗时和来源
- 构建缓存命中情况
- 守护进程的状态
我记得有一次,一个项目构建特别慢,用 Build Scan 一看,发现有个依赖每次都在从 Maven Central 重新下载,根本没走缓存。后来发现是版本号写成了 1.0.0-SNAPSHOT,每次构建都当新版本处理。改掉之后,构建时间直接降了 30%。
12.2 并行构建:让 CPU 忙起来
现在的电脑少说也是 4 核、8 核的。如果构建时只有一个核在工作,那其他核都在摸鱼。并行构建就是让它们都动起来。
12.2.1 开启并行构建
在 gradle.properties 里加上:
org.gradle.parallel=true
这个参数会让 Gradle 把相互独立的 Task 放到不同的线程里同时跑。比如,你项目里有三个 module,它们之间没有依赖关系,那就可以同时编译。
你还可以控制并行线程数:
org.gradle.workers.max=4
这个值一般设成 CPU 核心数就行。设太大反而会因为线程切换开销导致性能下降。
12.2.2 并行构建的坑
我曾经在一个项目里开了并行构建,结果发现构建时间反而变长了。排查了半天,发现是某个自定义 Task 里写了全局变量,多个线程同时读写,导致频繁锁等待。
所以,如果你用了并行构建,一定要确保你的 Task 是线程安全的。特别是那些自定义的 Transform、Annotation Processor,很容易踩坑。
12.3 守护进程调优:别让它频繁重启
Gradle 守护进程(Daemon)说白了就是一个一直在后台运行的 JVM 进程。你每次构建都复用这个进程,省去了 JVM 启动和类加载的时间。
12.3.1 检查守护进程状态
你可以用这个命令看看当前有哪些守护进程在跑:
./gradlew --status
输出大概长这样:
PID STATUS INFO
12345 BUSY 6.8.5
12346 IDLE 6.8.5
如果发现有很多个守护进程,说明你的项目可能在不同目录下分别启动了守护进程。这时候可以设置一个统一的守护进程目录:
org.gradle.daemon=true
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
12.3.2 守护进程的内存调优
守护进程默认的堆内存是 512MB。对于大型项目,这远远不够。我建议至少给到 2GB:
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError
这里有几个参数的含义:
| 参数 | 说明 | 建议值 |
|---|---|---|
| -Xmx | 最大堆内存 | 2048m 或更高 |
| -XX:MaxMetaspaceSize | 元空间上限 | 512m |
| -XX:+HeapDumpOnOutOfMemoryError | OOM 时自动 dump 堆 | 建议开启 |
你想想看,如果守护进程频繁 GC,那构建速度肯定受影响。给足内存,让 GC 少发生几次,效果立竿见影。
12.4 避免反模式:这些坑我替你踩过了
优化构建速度,有时候不是「做什么」,而是「不做什么」。下面这几个反模式,我几乎在每个项目里都见过。
12.4.1 不要在构建时动态生成代码
有些人喜欢在构建时用 Groovy 或 Kotlin 脚本动态生成 Java 文件。比如:
// 反模式示例
task generateCode {
doLast {
file("$buildDir/generated/Config.java").text = """
public class Config {
public static final String BUILD_TIME = "${new Date()}";
}
""".trim()
}
}
这样做的问题在于:每次构建都会重新生成这个文件,导致所有依赖它的 Task 都得重新执行。构建缓存完全失效。
正确的做法是:把这类信息放到 BuildConfig 里,或者用 res/values/strings.xml 来管理。
12.4.2 不要滥用 allprojects 和 subprojects
我见过不少项目,在根目录的 build.gradle 里写:
// 反模式示例
allprojects {
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
}
}
这个配置会应用到所有子模块,包括那些你根本不需要的模块。每次构建,每个模块的编译任务都会多执行一个参数检查。
更好的做法是:只在需要的模块里单独配置,或者用 afterEvaluate 按条件应用。
12.4.3 不要关闭构建缓存
有些人觉得构建缓存没用,或者嫌它占磁盘空间,就把它关了:
// 反模式示例
org.gradle.caching=false
这绝对是因小失大。构建缓存是 Gradle 最核心的优化手段之一。特别是当你只改了一个文件时,缓存能帮你省掉 90% 的编译时间。
我建议你不仅不要关,还要配置远程缓存,让 CI 和本地共享缓存结果:
// settings.gradle
buildCache {
local {
enabled = true
directory = new File(rootDir, '.gradle-build-cache')
}
remote(HttpBuildCache) {
url = 'https://your-cache-server/'
push = true
}
}
12.4.4 不要在依赖中使用动态版本
像 1.0.+ 或 latest.release 这样的动态版本,每次构建都会去远程仓库检查是否有新版本。这会导致:
- 构建时间变长(网络请求开销)
- 构建结果不可复现(不同时间可能拿到不同版本)
我曾经接手过一个项目,里面全是 2.3.+ 这样的写法。每次构建都要等 30 秒去检查版本更新。后来全部改成固定版本,构建时间直接降了 20%。
cacheChangingModulesFor(1, 'hours'),让 Gradle 每小时才检查一次。
12.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个检查清单,每次优化构建速度时对照着看。
好了,这一章的内容就到这里。构建优化是个持续的过程,不是调一次就一劳永逸的。我建议你每迭代几个版本,就重新跑一次 profile,看看有没有新的瓶颈出现。
记住一句话:别让开发者等构建。等构建的时间,就是浪费生命。