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% 的时间,那这就是你要优化的重点。

小技巧: 跑 profile 的时候,最好先 clean 一次。这样得到的数据更接近「冷启动」的真实情况。

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%。

注意: Build Scan 默认会把数据上传到 Gradle 的服务器。如果你有隐私顾虑,可以自建 Build Scan 服务,或者只在本地用 --profile。

12.2 并行构建:让 CPU 忙起来

现在的电脑少说也是 4 核、8 核的。如果构建时只有一个核在工作,那其他核都在摸鱼。并行构建就是让它们都动起来。

12.2.1 开启并行构建

gradle.properties 里加上:

org.gradle.parallel=true

这个参数会让 Gradle 把相互独立的 Task 放到不同的线程里同时跑。比如,你项目里有三个 module,它们之间没有依赖关系,那就可以同时编译。

你还可以控制并行线程数:

org.gradle.workers.max=4

这个值一般设成 CPU 核心数就行。设太大反而会因为线程切换开销导致性能下降。

核心要点: 并行构建不是万能的。如果项目里大部分 Task 都有依赖关系,那并行效果就有限。这时候你需要考虑的是「模块化拆分」。

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 少发生几次,效果立竿见影。

我的习惯: 每次升级 Gradle 版本后,我都会先跑一次构建,然后看守护进程的内存占用。如果发现占用超过 80%,就适当调大 -Xmx。

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 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个检查清单,每次优化构建速度时对照着看。

构建性能优化知识体系 1. 构建速度分析 • --profile 生成本地报告 • Build Scan 云端分析 • 关注 Task 耗时分布 • 识别瓶颈模块 2. 并行构建 • org.gradle.parallel=true • 控制 workers 数量 • 确保 Task 线程安全 • 模块化拆分提升并行度 3. 守护进程调优 • 检查守护进程状态 • 调整 JVM 堆内存 • 避免频繁重启 • 统一守护进程目录 4. 避免反模式 • 不在构建时动态生成代码 • 不滥用 allprojects • 不关闭构建缓存 • 不使用动态依赖版本 目标:减少等待,提升开发效率

好了,这一章的内容就到这里。构建优化是个持续的过程,不是调一次就一劳永逸的。我建议你每迭代几个版本,就重新跑一次 profile,看看有没有新的瓶颈出现。

记住一句话:别让开发者等构建。等构建的时间,就是浪费生命。

公众号:蓝海资料掘金营,微信 deep3321