Gradle 构建加速全攻略:从 5 分钟到 30 秒
Gradle 构建加速全攻略:从 5 分钟到 30 秒
"等构建"是 Android 开发者最浪费时间的事情之一。一个中型项目,冷构建动辄 3-5 分钟,热构建也要 30 秒以上,一天下来光等构建就消耗半小时。本文系统梳理 Gradle 构建加速的完整方法论——从配置调优到架构改造,从本地缓存到远程缓存,手把手带你把构建时间压缩到极致。
先诊断,再优化
盲目加配置不会让构建变快,反而可能引入新问题。优化第一步:搞清楚时间花在哪了。
开启 Build Scan
Gradle 官方的 Build Scan 是最权威的诊断工具:
// settings.gradle.kts
plugins {
id("com.gradle.enterprise") version "3.18"
}
gradleEnterprise {
buildScan {
termsOfServiceUrl = "https://gradle.com/terms-of-service"
termsOfServiceAgree = "yes"
publishAlways() // 每次构建都生成 scan
}
}
构建后会在终端输出一个链接,打开后可以看到:
- 每个任务的执行时间
- 任务依赖关系图
- 临界路径(Critical Path)
- 哪些任务可以并行但没有并行
使用 --profile 本地分析
不想上传数据到 Gradle 服务器?用本地 profile:
./gradlew assembleDebug --profile
构建结束后在 build/reports/profile/ 下生成 HTML 报告,包含:
- 任务执行时间排序
- 依赖解析耗时
- 配置阶段耗时
用 --scan 定位慢任务
# 精确定位哪个任务慢
./gradlew assembleDebug --scan
# 只看特定模块的构建时间
./gradlew :app:assembleDebug --profile
典型的时间分布
一个中型 Android 项目的构建时间通常这样分布:
配置阶段 (Configuration): 15-20% → 解析所有 build.gradle
依赖解析 (Resolution): 10-15% → 下载和解析依赖
注解处理 (Annotation Proc): 20-30% → KSP/KAPT、Room、Hilt 等
编译 (Compilation): 25-35% → Kotlin/Java 编译
Dex/R8: 10-15% → 字节码转换和优化
其他 (Resources/Assets): 5-10% → 资源处理、打包
优化策略要针对最大头下手。下面按收益从高到低排列。
基础配置优化:零成本提速
这些改动不需要改代码,不需要改架构,只需要调整 Gradle 配置。但效果可能出乎意料。
开启 Gradle 配置缓存
配置缓存(Configuration Cache)是 Gradle 7.x 引入的重磅特性:缓存配置阶段的结果,下次构建直接复用,跳过整个配置阶段。
// gradle.properties
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn // 先 warn,确保兼容后再改 fail
效果:冷构建配置阶段从 10-15 秒降到 1 秒以内。
但有一个前提:你的构建脚本必须是配置缓存兼容的。常见不兼容的情况:
// ❌ 不兼容:在配置阶段访问 Project 实例
tasks.register("myTask") {
val project = project // 配置缓存不允许
doLast { println(project.name) }
}
// ✅ 兼容:通过 Provider 传递
tasks.register("myTask") {
val projectName = providers.gradleProperty("projectName")
doLast { println(projectName.get()) }
}
开启并行构建
# gradle.properties
org.gradle.parallel=true # 允许不同子项目并行构建
org.gradle.workers.max=8 # 最大工作线程数(通常设为 CPU 核心数)
默认情况下 Gradle 是串行构建子项目的。开启并行后,没有依赖关系的模块可以同时编译。
效果:多模块项目构建时间减少 20-40%。
增大 Gradle 堆内存
# gradle.properties
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError
关键参数:
-Xmx4g:给 Gradle daemon 分配 4GB 堆内存(根据机器内存调整,建议不低于 2GB)-XX:+UseParallelGC:使用并行 GC,降低 GC 暂停-XX:+HeapDumpOnOutOfMemoryError:OOM 时 dump,方便排查
开启构建缓存
# gradle.properties
org.gradle.caching=true
或在命令行:
./gradlew assembleDebug --build-cache
构建缓存会复用之前相同输入的 task 输出。切换分支后再构建时,没改动的模块直接从缓存取结果。
完整的 gradle.properties 优化模板
# === JVM 配置 ===
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# === 并行与缓存 ===
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
# === Daemon ===
org.gradle.daemon=true # 确保 daemon 开启(默认就是 true)
# === Android 特定 ===
android.useAndroidX=true
android.nonTransitiveRClass=true # 非传递 R 类,减少 R 类体积
# === Kotlin ===
kotlin.incremental=true # Kotlin 增量编译(默认 true)
kotlin.incremental.java=true # Java 变更也触发增量编译
kotlin.caching.enabled=true
# === 调试 ===
# org.gradle.debug=true # 只在需要调试时开启
# org.gradle.logging.level=debug # 太多日志反而慢
依赖优化:砍掉看不见的拖累
检测未使用的依赖
依赖越多,解析越慢,编译也越慢(传递依赖会参与编译 classpath)。
# 使用 gradle-dependency-analyze 插件
./gradlew :app:analyzeDependencies
// build.gradle.kts
plugins {
id("ca.neitsch.dependencyanalyze") version "1.7.0"
}
手动排查:
# 查看依赖树,找出异常大的传递依赖
./gradlew :app:dependencies --configuration debugRuntimeClasspath | grep "→"
# 查看编译期依赖(这个最影响构建速度)
./gradlew :app:dependencies --configuration debugCompileClasspath
排除不必要的传递依赖
// ❌ 引入了一堆不需要的传递依赖
implementation("com.squareup.retrofit2:retrofit:2.11.0")
// ✅ 精确控制
implementation("com.squareup.retrofit2:retrofit:2.11.0") {
// 排除不需要的模块
exclude(group = "com.squareup.okhttp3", module = "okhttp")
}
// 单独引入需要的版本
implementation("com.squareup.okhttp3:okhttp:4.12.0")
使用 compileOnly 替代 implementation
如果一个依赖只在编译期使用(运行时由其他途径提供),用 compileOnly:
// Lombok 只在编译期用
compileOnly("org.projectlombok:lombok:1.18.34")
annotationProcessor("org.projectlombok:lombok:1.18.34")
// 注解只在编译期用
compileOnly("javax.annotation:javax.annotation-api:1.3.2")
compileOnly 的依赖不会进入运行时 classpath,减少 dex 工作量。
统一依赖版本,避免冲突解析
版本冲突会让 Gradle 花大量时间解析该用哪个版本:
// versionCatalog 统一管理(推荐)
// gradle/libs.versions.toml
[versions]
kotlin = "2.0.21"
compose-bom = "2024.12.01"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
# 使用
dependencies {
implementation(libs.kotlin.stdlib)
implementation(platform(libs.compose.bom))
}
KAPT → KSP:注解处理的质变
KAPT 为什么慢
KAPT(Kotlin Annotation Processing Tool)的工作流程:
Kotlin 源码 → 生成 Java 存根(Stub)→ Java 注解处理器处理 → 生成代码 → Kotlin 编译器编译
↑ 这是瓶颈!
KAPT 必须先把 Kotlin 代码转成 Java 存根,才能让基于 Java 的注解处理器工作。这个"翻译"过程非常慢,而且不支持增量编译。
KSP 的优势
KSP(Kotlin Symbol Processing)直接在 Kotlin 编译器上运行:
Kotlin 源码 → KSP 直接解析 Kotlin AST → 生成代码 → Kotlin 编译器编译
- 不需要生成 Java 存根
- 支持增量处理
- 速度通常是 KAPT 的 2 倍以上
迁移步骤
// 1. 替换插件
// 旧:kotlin("kapt")
// 新:
plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.28"
}
// 2. 替换依赖声明
// 旧:kapt("com.google.dagger:hilt-compiler:2.51.1")
// 新:
ksp("com.google.dagger:hilt-compiler:2.51.1")
// 3. 移除 kapt 配置
// 删除 kapt { ... } 块
已支持 KSP 的主流库
| 库 | KSP 支持 | 迁移难度 |
|---|---|---|
| Room | ✅ 2.4+ | 简单 |
| Hilt/Dagger | ✅ 2.48+ | 简单 |
| Moshi | ✅ 简单 | 简单 |
| Epoxy | ✅ 5.0+ | 中等 |
| Glide | ✅ 4.16+ | 简单 |
| Lombok | ❌ 不支持 | 无法迁移(换掉 Lombok) |
实测效果
项目:~50 个模块,Room + Hilt + Moshi
KAPT 冷构建:3 分 12 秒
KSP 冷构建:1 分 48 秒 (-44%)
KAPT 增量:42 秒
KSP 增量:18 秒 (-57%)
如果只能做一件事,就从 KAPT 迁移到 KSP。
增量编译:只编译改动的部分
Kotlin 增量编译
Kotlin 增量编译默认开启,但有些操作会触发全量编译:
# gradle.properties(确认开启)
kotlin.incremental=true
kotlin.incremental.java=true
kotlin.incremental.js=true # KMP 项目
会破坏增量编译的操作:
- 修改 build.gradle.kts 中的编译选项
- 修改依赖版本
- 修改 Kotlin 编译器插件配置
- 修改注解处理器的输入/输出
Kotlin 增量编译的"classpath snapshot"
Gradle 8.x + Kotlin 2.x 使用新的增量编译机制:
旧机制:比较源文件的修改时间
新机制:比较编译产物的 ABI(Application Binary Interface)
好处:改了内部实现但没改 ABI → 下游模块不需要重新编译
// build.gradle.kts
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
// 开启 ABI 增量(Gradle 8.2+ 默认开启)
freeCompilerArgs.add("-Xabi-gen")
}
}
资源增量编译
Android 资源处理也有增量支持:
// build.gradle.kts (app)
android {
buildFeatures {
// 减少资源合并时间
renderScript = false // 不用就关掉
aidl = false // 不用就关掉
buildConfig = true // 需要就保留
}
}
模块化:构建加速的架构级方案
为什么模块化能加速构建
单模块项目:改一行代码,整个项目重新编译。
多模块项目:改一个模块的代码,只重新编译该模块 + 依赖它的模块。
单模块:
app (2000 文件) → 改 1 个文件 → 编译 2000 文件
多模块:
:core (200 文件)
:feature-a (300 文件) ← 改了这里
:feature-b (300 文件) ← 不需要重新编译
:app (200 文件) ← 只重新编译 :feature-a 和 :app
模块化的正确姿势
按功能拆分,不要按层拆分:
❌ 按层拆分(变化时多模块同时改):
:data
:domain
:presentation
✅ 按功能拆分(变化时只改一个模块):
:feature:login
:feature:home
:feature:profile
:core:network
:core:database
避免循环依赖
循环依赖会导致 Gradle 无法确定构建顺序,丧失并行构建的优势:
// ❌ 循环依赖
:feature-a → :feature-b → :feature-a
// ✅ 提取公共模块
:feature-a → :core:common ← :feature-b
检测循环依赖:
./gradlew :app:dependencies --configuration debugRuntimeClasspath | grep "↳"
API 模块 vs Implementation 模块
// :core:network 对外暴露的接口
// :core:network-api 模块(只有接口,没有实现)
dependencies {
api(project(":core:network-api")) // 对外暴露
implementation(project(":core:network-impl")) // 内部实现
}
这样依赖 :core:network-api 的模块不需要因为网络实现变了而重新编译。
远程构建缓存:团队级加速
搭建远程缓存
远程缓存让整个团队共享构建产物——你构建过的模块,同事直接下载,不用重新编译。
方案一:Gradle Build Cache Node
// settings.gradle.kts
buildCache {
local {
directory = File(rootDir, ".gradle/build-cache")
removeUnusedEntriesAfterDays = 30
}
remote<HttpBuildCache> {
url = uri("https://cache.example.com/cache/")
push = System.getenv("CI") == "true" // CI 推送,开发者只拉取
credentials {
username = System.getenv("CACHE_USER")
password = System.getenv("CACHE_PASSWORD")
}
}
}
方案二:S3 作为缓存后端
remote<HttpBuildCache> {
url = uri("https://your-bucket.s3.amazonaws.com/cache/")
push = System.getenv("CI") == "true"
}
方案三:自建 Gradle Enterprise
企业级方案,包含 Build Scan + 远程缓存 + 预测性测试选择:
Gradle Enterprise
├── Build Cache (远程缓存)
├── Build Scan (构建分析)
├── Test Distribution (测试分发)
└── Predictive Test Selection (智能测试选择)
缓存命中率优化
远程缓存只有命中才有用。影响命中率的因素:
| 因素 | 说明 | 优化 |
|---|---|---|
| 任务输入不稳定 | 包含时间戳、随机数的输入 | 固定输入,排除不稳定字段 |
| 绝对路径 | 不同机器路径不同 | 使用相对路径 |
| 环境变量 | 不同机器环境不同 | 用 @Input 声明关键环境变量 |
| 原生代码 | 不同 CPU 架构产物不同 | 分架构缓存 |
// 自定义 Task 的缓存键
abstract class GenerateProtoTask : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE) // 用相对路径
abstract val protoFile: RegularFileProperty
@get:Input
abstract val version: Property<String> // 声明版本为输入
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
// ...
}
}
CI 构建优化
CI 环境的构建优化和本地不同——CI 每次都是干净的,没有增量编译的优势。
分阶段构建
# GitHub Actions 示例
jobs:
build:
steps:
# 1. 只编译,不打包 APK
- name: Compile
run: ./gradlew compileDebugKotlin --build-cache
# 2. 单元测试(可以和编译并行)
- name: Unit Test
run: ./gradlew testDebugUnitTest --build-cache
# 3. 打包 APK(只在需要时执行)
- name: Assemble
if: github.ref == 'refs/heads/main'
run: ./gradlew assembleDebug --build-cache
缓存 Gradle 依赖和构建缓存
# GitHub Actions
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
.gradle/build-cache
key: gradle-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml') }}
restore-keys: |
gradle-
只构建变更的模块
# 只构建变更影响的模块
CHANGED_MODULES=$(git diff --name-only origin/main...HEAD | \
grep -oP '^\K[^/]+' | sort -u | \
awk '{print ":"$1":compileDebugKotlin"}' | tr '\n' ' ')
if [ -n "$CHANGED_MODULES" ]; then
./gradlew $CHANGED_MODULES
fi
分布式测试执行
// Gradle Enterprise 的预测性测试选择
gradleEnterprise {
buildCache {
local { isEnabled = true }
remote { url = uri("https://cache.example.com/") }
}
predictions {
// 只运行可能受影响的测试
enabled = true
}
}
高级优化:深入编译器
Kotlin 编译器选项调优
// build.gradle.kts
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
freeCompilerArgs.addAll(
"-Xjvm-default=all", // 生成默认方法(减少生成类数量)
"-Xno-optimized-callable-references", // 跳过 callable reference 优化
"-Xsam-conversions=class", // SAM 转换策略
)
// 增量编译优化
incremental = true
}
}
R8 全模式优化
// build.gradle.kts (app)
android {
buildTypes {
release {
isMinifyEnabled = true // 开启 R8
isShrinkResources = true // 资源压缩
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
R8 在 release 构建中减少 dex 数量,间接减少下次构建的 dex 时间。
DexBuilder 增量处理
# gradle.properties
android.enableDexingArtifactTransform.desugaring=true
android.enableParallelDex=true # 并行 dex(AGP 8.x+ 默认开启)
Android Gradle Plugin 优化
AGP 配置优化
// build.gradle.kts (app)
android {
// 关闭不需要的构建变体
variantFilter {
if (name != "debug" && name != "release") {
ignore = true
}
}
// 减少打包内容
packaging {
resources {
excludes += setOf(
"META-INF/DEPENDENCIES",
"META-INF/LICENSE",
"META-INF/LICENSE.txt",
"META-INF/license.txt",
"META-INF/NOTICE",
"META-INF/NOTICE.txt",
"META-INF/notice.txt",
"META-INF/ASL2.0",
"META-INF/*.kotlin_module"
)
}
}
// 关闭不需要的 feature
buildFeatures {
buildConfig = true
viewBinding = true
dataBinding = false // 不用就关
compose = true
resValues = false # 不需要动态资源
}
}
非传递 R 类
# gradle.properties
android.nonTransitiveRClass=true
这个配置让每个模块只包含自己声明的资源 ID,不包含依赖模块的资源 ID。效果:
- 减少每个模块 R 类的大小
- 减少资源合并时间
- 资源变更不会导致所有模块重新编译
按 namespace 分离资源
// build.gradle.kts (library module)
android {
namespace = "com.example.core.network" // 明确声明 namespace
resourcePrefix = "network_" // 资源前缀,避免冲突
}
实战案例:一个真实项目的优化历程
项目概况
- 35 个模块(5 个 core + 20 个 feature + 10 个 legacy)
- Room + Hilt + Moshi + KAPT
- 冷构建 4 分 50 秒,增量构建 55 秒
优化步骤与效果
第一阶段:零成本配置优化
├── 开启 configuration-cache → 冷构建 -18 秒
├── 开启 parallel + 调大 JVM → 冷构建 -25 秒
├── 开启 build-cache → 增量 -12 秒
└── android.nonTransitiveRClass=true → 增量 -5 秒
小计:冷 3 分 47 秒,增量 38 秒
第二阶段:KAPT → KSP 迁移
├── Room KSP 迁移 → 冷构建 -45 秒
├── Hilt KSP 迁移 → 冷构建 -30 秒
└── Moshi KSP 迁移 → 冷构建 -15 秒
小计:冷 2 分 17 秒,增量 18 秒
第三阶段:依赖清理 + 模块拆分
├── 移除 12 个未使用的依赖 → 冷构建 -10 秒
├── 拆分 3 个巨型 feature 模块 → 增量 -6 秒
└── API/Implementation 分离 → 增量 -4 秒
小计:冷 2 分 07 秒,增量 8 秒
第四阶段:远程缓存 + CI 优化
├── 搭建远程缓存 → CI 冷构建 -60%
├── CI 分阶段构建 → PR 检查时间 -40%
└── 预测性测试选择 → 测试时间 -50%
最终结果
优化前 优化后 提升
冷构建 4 分 50 秒 2 分 07 秒 -56%
增量构建 55 秒 8 秒 -85%
CI PR 检查 12 分钟 4 分 30 秒 -63%
构建优化检查清单
立即可做(10 分钟内)
- [ ] 确认
gradle.properties中的 JVM 参数(-Xmx4g -XX:+UseParallelGC) - [ ] 开启
org.gradle.parallel=true - [ ] 开启
org.gradle.caching=true - [ ] 开启
android.nonTransitiveRClass=true - [ ] 运行一次
--profile,了解当前基线
本周可做
- [ ] 开启
org.gradle.configuration-cache=true - [ ] 检测并移除未使用的依赖
- [ ] 关闭不需要的
buildFeatures - [ ] 统一版本目录(Version Catalog)
本月可做
- [ ] KAPT → KSP 迁移
- [ ] 拆分巨型模块
- [ ] API/Implementation 分离
- [ ] 搭建远程构建缓存
长期规划
- [ ] 全面模块化(按功能拆分)
- [ ] 引入 Gradle Enterprise
- [ ] CI 分布式构建
- [ ] 定期 Build Scan 审查
常见误区
误区一:"升级 Gradle 版本就能变快"
升级确实有帮助,但不是万能的。Gradle 8.x 相比 7.x 在配置缓存和增量编译上有改进,但如果你的构建脚本不兼容配置缓存,升级也用不上。
正确做法:升级后先确认新特性是否生效,用 Build Scan 对比。
误区二:"模块越多越快"
模块化有收益边界。过多的小模块会增加:
- 配置阶段时间(每个模块都要解析 build.gradle)
- 依赖解析时间(模块间依赖变多)
- Kotlin 编译的 classpath 查找时间
经验值:一个模块 50-200 个源文件比较合理。少于 30 个文件的模块考虑合并。
误区三:"开越多线程越快"
org.gradle.workers.max 不是越大越好。太多线程会导致:
- CPU 上下文切换开销
- 内存压力增大(每个 worker 有独立类加载器)
- GC 压力
经验值:设为 CPU 核心数或 CPU 核心数 - 1。
误区四:"远程缓存一定比本地快"
远程缓存受网络延迟影响。在低延迟网络中确实有效,但在高延迟或不稳定网络中,下载缓存可能比本地编译还慢。
正确做法:CI 环境推送缓存,开发者环境设置 push = false 只拉取。
结语
构建优化不是一次性的工作,而是持续的过程。项目的代码在增长,依赖在变化,构建时间也会慢慢膨胀。关键不是一次优化到位,而是建立可观测性和持续优化的习惯。
我的建议是:
- 先量后改:用 Build Scan 或
--profile量化当前状态 - 先低后高:先做零成本配置优化,再做架构级改造
- 先人后机:先优化开发者日常的增量构建,再优化 CI 的冷构建
- 定期体检:每月跑一次 Build Scan,监控构建时间趋势
构建时间每减少 10 秒,假设团队 10 人每天构建 20 次,一年就省下了:
10 秒 × 10 人 × 20 次 × 250 工作日 = 500,000 秒 ≈ 138 小时 ≈ 17 个工作日
这 17 天,够写多少有价值的功能了。
参考资源: - Gradle 官方文档 — Performance Tuning - Android Developers — Reduce build times - 《Gradle in Action》— Benjamin Muschko - Google I/O 2025 — What's new in Android Gradle Plugin - Gradle Build Cache 最佳实践