Android Gradle 构建提速全链路:从 Configuration Cache 到 KSP 替换 KAPT 的工程化实践

一个中型项目,30 个模块,clean build 跑 4 分钟,增量构建也要 1 分 20 秒。这类数字在稍具规模的团队里并不少见,但多数人的应对方式是”加机器内存”或者”升级 Gradle 版本祈祷变快”。

构建耗时不是一个整体,而是三段叠加:配置阶段(Configuration Phase)注解处理(Annotation Processing)任务执行(Task Execution)。三段的瓶颈原因完全不同,混着优化等于同时修三台机器的不同故障——听起来很忙,实际上进展有限。

把这三段拆开,逐段定位,逐段解决。


第一步:摸清时间花在哪里

盲目优化是最贵的时间浪费。Gradle 自带了足够好的诊断工具,但很多人从没用过。

--profile 跑一次构建,Gradle 会在 build/reports/profile/ 下生成 HTML 报告,包含配置阶段耗时和每个 task 的执行时间分布。更细粒度的分析用 --scan

./gradlew assembleDebug --profile
./gradlew assembleDebug --scan  # 上传到 scans.gradle.com,获取交互式分析报告

从报告里找三个关键数字:

  • Configuration time:超过 10s,配置阶段有问题
  • Task :app:kaptDebugKotlin 耗时:KAPT 的重灾区,通常是单任务里耗时最长的
  • 并行任务数量:大量 task 串行排队,说明依赖图或并行配置有问题

没有 profile 数据就动手,大概率优化了一个根本不是瓶颈的地方。


配置阶段:Configuration Cache

配置阶段是 Gradle 解析所有 build.gradle 文件、构建 task 依赖图的过程。每次构建都会重新执行,哪怕只改了一行业务代码。

40 个模块的项目,配置阶段轻松吃掉 15–20 秒。Configuration Cache(CC)的思路是把这份 task 图序列化缓存,下次构建时如果 build.gradle 没有变化,直接反序列化跳过配置阶段。

启用方式:

# gradle.properties
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn  # 先用 warn 模式,排查兼容问题

CC 兼容性:真正的工作量

CC 的难点不在开启,在兼容性修复。Gradle 要求 task 的所有输入输出在序列化时必须是 CC 安全的,以下三类写法会直接导致 CC 失效或行为异常:

在 task action 里直接访问 project 对象:

// ❌ task action 执行时 project 不可序列化
tasks.register("generateConfig") {
    doLast {
        val flavor = project.properties["flavor"]
    }
}

// ✅ 提前捕获为 Provider,声明为 @Input
tasks.register("generateConfig") {
    val flavor = providers.gradleProperty("flavor")
    doLast {
        val f = flavor.orNull
    }
}

使用 afterEvaluate 修改 task 状态:afterEvaluate 是配置阶段的副作用,CC 环境下行为不可预测。替代方案是用 Provider 延迟求值,或者重新梳理 task 依赖关系,通过声明式 API 表达依赖。

第三方插件不兼容: 实际项目里最头疼的部分。AGP 8.x 已全面兼容 CC,但一些老旧的代码生成插件(某些路由框架的 Gradle 插件)还没跟上。先用 warn 模式跑一遍,从报告里找不兼容的插件,逐个升级或替换。

实测下来,升级 AGP 到 8.1+ 并开启 CC 后,40 模块项目的增量构建配置时间从 18s 降到 2s 以内。这是收益最直接的一项优化,代价是一到两天的兼容性排查工作。


注解处理:KSP 替换 KAPT

KAPT(Kotlin Annotation Processing Tool)的工作原理决定了它天生慢:把 Kotlin 源码编译成 Java Stub → 把 Stub 喂给 Java APT 处理器 → 生成代码后再做一次完整编译。三步走,每一步都是实打实的编译开销。

一个模块的 kaptDebugKotlin task 在中型项目里耗时 15–30s 很正常,多个模块叠加就是分钟级的浪费。

KSP(Kotlin Symbol Processing) 直接作为 Kotlin 编译器插件运行,在符号解析层面处理注解,完全跳过 Java Stub 生成。官方数据是 2x 提升,大厂实测普遍在 2x–4x,这个数字有实际支撑。

迁移步骤

以 Room 和 Hilt 为例:

// 根 build.gradle.kts
plugins {
    id("com.google.devtools.ksp") version "2.0.0-1.0.21" apply false
}
// 模块 build.gradle.kts
plugins {
    id("com.google.devtools.ksp")
}

dependencies {
    // 把 kapt 改成 ksp,其他依赖不变
    ksp("androidx.room:room-compiler:2.6.1")
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")

    ksp("com.google.dagger:hilt-compiler:2.51")
    implementation("com.google.dagger:hilt-android:2.51")
}

迁移完后把所有模块的 kotlin("kapt") 插件声明删掉。混用 KAPT 和 KSP 处理同一个库会导致两套处理器都跑一遍,比单独用 KAPT 还慢。

不是所有库都已支持 KSP

Glide 的 KSP 支持长期处于 experimental 状态;国内部分路由框架(ARouter 旧版)依赖 KAPT,KSP 版本由社区维护,稳定性需要自行评估。迁移前查官方文档确认支持状态,不要抱着”试试能不能跑”的心态直接上——生成代码的差异在编译期不一定报错,运行时出问题才更难排查。


任务执行:增量编译与并行构建

增量编译的隐蔽杀手

Gradle 增量编译依赖两件事:task 的输入输出声明正确,以及 UP-TO-DATE 检查不被意外破坏。

踩过一个典型坑:有个自定义 task 把 System.currentTimeMillis() 写进了输出文件的注释里,导致每次输出都变化,这个 task 永远不是 UP-TO-DATE,下游所有依赖它的 task 也全部重新执行。定位方法:

./gradlew assembleDebug --info 2>&1 | grep "not up-to-date\|UP-TO-DATE"

自定义 task 的输入输出必须显式声明:

abstract class GenerateVersionTask : DefaultTask() {
    @get:Input
    abstract val versionCode: Property<Int>

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun execute() {
        // ✅ 输出只依赖声明的 @Input,不引入时间戳等外部不确定因素
        outputFile.get().asFile.writeText("VERSION_CODE=${versionCode.get()}")
    }
}

并行执行与 JVM 调优

# gradle.properties
org.gradle.parallel=true
org.gradle.workers.max=8        # 建议 = CPU 核数,IO 密集型任务可以适当超一点
org.gradle.caching=true         # 开启 Build Cache,与 CC 独立,两者可同时启用
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

并行构建发挥效果的前提是模块化拆分合理。所有业务代码堆在一个 :app 模块,并行根本没有发挥空间——这是架构问题,配置参数解决不了。

Build Cache 在 CI 环境收益最大:同一份代码、不同分支切换时,大量 task 可以从缓存恢复而不重新执行。多机 CI 可以配置远程缓存节点,一台机器的构建结果可以被其他机器直接复用。


三项优化的优先级和预期收益

从实测数据看,三类优化的收益差异明显:

优化项增量构建收益实施难度
KSP 替换 KAPT注解处理 -50%–70%低(按库逐步迁移)
Configuration Cache配置时间 -85%+中(需排查兼容问题)
并行 + 增量修复任务执行 -20%–40%低~中

我更倾向于先做 KSP 迁移:风险低,收益即时,按库分批推进,失败了回滚也容易。改几行依赖声明,构建时间直接少掉 20–30 秒,性价比最高。

Configuration Cache 的收益依赖项目规模,模块越多、配置越复杂,效果越显著。少于 10 个模块的小项目可以先观望;大项目建议把 CC 兼容性修复单独排一个迭代,不要混在功能开发里做——排查问题时上下文混乱会让工作量翻倍。

还在用 AGP 7.x 的项目,可以把升级 AGP 8.x 一并规划进来。CC 稳定支持、buildFeatures 精细控制(关掉不用的 buildConfigaidlrenderScript)、R 文件生成优化,这些在 AGP 8 里都有实质性改善。升级本身有迁移成本,但收益是系统性的,值得做。

延伸阅读