构建为什么越来越慢
一个中型安卓工程的全量构建可能长达数分钟,其中真正编译代码的时间往往只占一小部分,其余消耗在:解析与下载依赖、处理注解、打包资源、执行自定义任务,以及每次构建都要重新计算的配置阶段。
Gradle 的构建过程分为三个阶段:初始化(决定参与构建的项目)、配置(执行所有 build script,构建任务图)、执行(真正跑任务)。优化要分清是哪个阶段慢,因为它们的解法完全不同。
先测量再优化。带 --profile 或构建扫描跑一次,看清时间花在哪,再决定改什么——凭空猜测往往优化错了地方。
下面按依赖、执行、配置三条线索展开,最后给出一份可直接套用的配置。
依赖解析与仓库缓存
依赖是构建阶段最常见的耗时来源,尤其在网络不稳时。核心思路是:让依赖更快被找到,并且更少被重新下载。
- 声明明确的版本,避免动态版本(如 1.+):动态版本每次都要检查远程仓库,且构建不可复现。
- 用 dependencyResolutionManagement 统一仓库配置,避免每个模块各写一份导致重复查询。
- 启用依赖缓存与离线模式:网络受限时可以加 --offline 复用本地缓存。
- 减少不必要的依赖传递:用
implementation而非api,避免把内部依赖暴露给上层模块、扩大重编译范围。
api 与 implementation 的选择影响很大:api 声明的依赖会参与上层模块的编译,改动它会引发更大范围的重编译。除非确实是模块对外接口的一部分,否则一律用 implementation。
增量构建与注解处理
增量构建的目标是“只重做受改动影响的任务”。Gradle 与安卓插件都支持增量编译,但前提是你的自定义任务正确定义了输入输出。
- 任务输入输出自定义任务必须声明 @Input/@Output,否则每次都被视为脏任务全量执行。
- 注解处理器每个处理器都会显著增加编译时间,能不用就不用;kapt 比 KSP 慢得多,能用 KSP 就换。
- 非增量注解部分处理器不支持增量,会让整个编译退化,注意编译日志中的提示。
- 资源处理关闭不必要的 PNG 压缩与矢量转换,可减少资源处理时间。
一个立竿见影的改动是把 kapt 迁移到 KSP:在同时使用房间(Room)等注解框架的项目里,编译时间通常能缩短三成以上,而且迁移成本通常只有改几行配置。
配置缓存:跳过配置阶段
每次构建都要重新执行所有 build script 来构建任务图,这在多模块工程里可能就要好几秒。配置缓存(Configuration Cache)把这一步的结果缓存起来,下次直接复用。
启用方式很简单:在 gradle.properties 中打开开关。但它对构建脚本有要求——不能在配置阶段访问项目运行时的状态、不能依赖绝对路径、不能在配置期执行 IO。刚开始启用时通常会报一批错,逐条修完,收益非常可观。
与之配套的还有任务缓存(Build Cache):把任务输出缓存到本地目录甚至远端,多人协作时可以直接复用别人构建过的任务产物。CI 环境尤其值得开启,配合远端缓存能让流水线时间大幅下降。
构建扫描:找出瓶颈任务
# 生成构建概览(本地)
./gradlew assembleDebug --profile
# 输出各阶段耗时摘要(不看可视化报告)
./gradlew assembleDebug --profile --scan
# 查看为什么某个模块每次都重新构建
./gradlew assembleDebug --info | grep -i "not up-to-date"
# 单独测量配置阶段耗时
./gradlew help --profile
构建扫描会给出每个任务的耗时排序、缓存命中情况以及“为什么任务不是最新的”的具体原因。多数情况下,排在前几名的任务只有两三个,优化它们就够了。
常见的发现包括:某个模块的注解处理拖慢了整个构建;某个自定义任务因为未声明输入而每次全量执行;资源处理在每次改动 Kotlin 代码时都被重新执行。这些问题一旦定位,修复成本通常很低。
变体与按需构建
安卓的构建变体(Variant)会按 productFlavors × buildTypes 组合生成任务。变体越多,任务图越大,配置与执行时间越长。
- 开发期只构建需要的变体:
./gradlew assembleDebug指定具体变体,不要让 IDE 构建全部。 - 减少不必要的 flavor 维度,能合并的合并;用 flavorDimensions 精简组合数量。
- 用
androidComponents的变体 API 按需启用任务,而不是在 afterEvaluate 里动态改依赖。 - 开启按需配置(Configure on Demand)在多模块工程中可减少配置阶段的工作量。
另一个容易被忽略的点是测试任务:./gradlew build 会连带跑 lint 与所有单元测试,日常开发用 assembleDebug 就够了,把完整检查留给 CI。
实战:一份可复用的提速配置
把上面的手段汇总成两份配置,可以直接拷进项目使用。
# gradle.properties —— 构建提速配置
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+UseParallelGC -Dfile.encoding=UTF-8
org.gradle.parallel=true # 并行执行任务
org.gradle.caching=true # 启用构建缓存
org.gradle.configuration-cache=true # 启用配置缓存
org.gradle.configureondemand=true # 按需配置(多模块工程)
kotlin.incremental=true # Kotlin 增量编译
kotlin.incremental.useClasspathSnapshot=true
android.nonTransitiveRClass=true # 非传递 R 类,减小重编译范围
android.enableJetifier=false # 新工程不再需要 Jetifier
android.defaults.buildfeatures.buildconfig=false
// app/build.gradle.kts —— 关闭非必要任务、改用 KSP
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp) // 用 KSP 替代 kapt
}
android {
buildFeatures {
buildConfig = false // 不需要 BuildConfig 时关闭
viewBinding = true
}
packaging {
resources.excludes += setOf("META-INF/*.kotlin_module", "DebugProbesKt.bin")
}
testOptions {
unitTests.isIncludeAndroidResources = true
}
}
// 开发期跳过 lint 等重任务(CI 上单独执行)
tasks.matching { it.name.startsWith("lint") }.configureEach {
enabled = !providers.gradleProperty("skipLint").isPresent
}
注意 jvmargs 的堆大小要结合实际机器内存调整:给太小会频繁 GC,给太大会挤占系统内存。建议从 2g 起步,观察构建日志中的 GC 情况再调整。
持续集成中的构建优化
CI 上的构建环境通常每次都是全新的,所以缓存策略比本地更重要。
- 缓存 Gradle 用户目录与构建缓存目录,配合 key 包含 gradle-wrapper 与构建脚本的哈希。
- 开启远端构建缓存,让本地与 CI 互相复用任务产物。
- 拆分任务:静态检查、单元测试、打包分别作为独立步骤,失败时能快速定位,也便于并行。
- 固定 JDK 与 Gradle 版本,避免环境差异带来的偶发失败。
本地快、CI 慢,通常是缓存没配好;CI 快、本地慢,通常是本地 JVM 参数或增量没开。两边用同一套配置思路,差距会明显缩小。
最后提醒一点:构建提速的改动也需要验证正确性。启用配置缓存后,务必跑一次完整的打包与测试,确认产物没有任何差异再合并。