Gradle 构建慢的问题,有人找到了新解法

Gradle 构建慢的问题,有人找到了新解法

Gradle 构建慢的问题,有人找到了新解法


「Gradle 构建慢的问题,有人找到了新解法」


从那个熟悉的 coffee break 说起


每个 Android 开发者都经历过这种时刻:按完 Run 键,看着 Gradle 进度条开始蠕动,然后起身去接水、上厕所、跟同事寒暄一圈回来,发现还在 "Resolve dependencies for :app:debugCompileClasspath"。这甚至成了一个内部梗——"Gradle build 是程序员最好的休息提醒"。


但玩笑归玩笑,构建耗时是真实侵蚀生产力的事。Google 官方数据说大型项目 clean build 动辄十几分钟,增量构建也要几十秒到数分钟。这些年社区给出的解法堆成山:Gradle Daemon、Build Cache、Configuration Cache、并行编译、模块化拆工程、把 AGP 升到最新版……效果有,但天花板明显。Configuration Cache 从实验到稳定花了多少个版本?Build Cache 命中率的玄学谁没吐槽过。你配置拉满,该慢的时候照样慢,尤其是 CI 环境每次都要重新下载依赖、重新解析变体,那酸爽。


所以当我看到 JetBrains 和 Gradle 官方开始推 Kotlin-based Gradle DSL 的 performance 优化,以及更近期社区里冒出来的几个新方向时,第一反应是:又来?第二反应是:这次可能真的有点不一样。


Kotlin DSL 不是新东西,但性能优化是新战场


先说清楚,Kotlin DSL(build.gradle.kts)取代 Groovy DSL(build.gradle)这件事本身已经不新鲜。Android Studio 新建项目默认就是 .kts,Google 官方模板也迁移了好几年。但早期社区对 Kotlin DSL 的抱怨集中在"编译脚本更慢"——因为 Kotlin 脚本需要先编译再执行,Groovy 是解释执行,启动开销理论上更小。


这个认知在 Gradle 7.x 时代基本成立。我当时试过把一个中等规模的模块化项目全量迁移到 Kotlin DSL,clean build 时间确实涨了 10-15%,配置阶段(configuration phase)耗时尤其明显。Gradle 官方 issue tracker 里 #16603、#15886 这些 ticket 底下,抱怨脚本编译缓存失效、K2 编译器支持滞后的评论攒了几百条。


转折点发生在 Gradle 8.0 和 Kotlin 1.9.x 这个组合上。JetBrains 的 Vladimir Dolzhenko 在 2023 年底的 KotlinConf 上放了一个数据:Kotlin DSL 的脚本编译缓存(compiled script cache)命中率在 Gradle 8.0 配合新版 Kotlin Gradle Plugin 后有了质变,配置阶段耗时在某些大型项目上反超 Groovy DSL。原理不复杂——Groovy 的解释执行在脚本变复杂后边际成本递增,而 Kotlin 的预编译脚本加上更激进的缓存策略,把"一次编译、多次复用"的优势放大了。


我手头没有超大型单体工程来验证这个反超,但把个人维护的一个 30 模块的 side project 从 Gradle 7.5 + Groovy 升到 8.4 + Kotlin DSL 后,configuration time 从平均 8 秒降到 4 秒左右,incremental build 的感知延迟确实小了。这个样本很小,不足以支撑宏大结论,但至少说明官方不是在纯画饼。


更关键的是,Kotlin DSL 的静态类型优势开始反哺构建性能。Groovy 的灵活语法是双刃剑,动态解析在运行时才能暴露问题,而 Kotlin 的编译期检查让 Gradle 能更早做依赖图优化。Gradle 8.1 引入的 buildSrccomposite build 的迁移建议、8.2 对 settings.gradle.kts 里 pluginManagement 的解析优化,底层都依赖 Kotlin DSL 的 AST 可分析性。这些是 Groovy 时代想做但做不利索的事。


Gradle Enterprise 的免费 tier 和社区版替代


构建优化的另一个维度是"可观测性"。你都不知道时间耗在哪,优化个鬼。Gradle Enterprise(现在叫 Develocity)一直是这领域的标杆产品,构建扫描(build scan)能细化到每个 task 的输入输出快照、缓存命中情况、依赖下载耗时。但企业版价格……这么说吧,我接触过的几家国内中型互联网公司,评估完报价后默默继续用日志 grep。


2023 年 Gradle 推出了 Develocity 的免费 tier,限制是每月 3 次 build scan 保留,历史数据 30 天。对个体开发者和小团队够用了,但稍微上点规模的团队还是卡脖子。真正有意思的变化是社区替代方案的成熟。


Kotlin 社区里长出来的 gradle-profiler 是个老工具了,但结合新的 Chrome trace 输出格式,现在能画出比 Gradle Enterprise 更细粒度的火焰图。我去年排查一个 :app:mergeDebugNativeLibs 莫名耗时的问题,就是用 gradle-profiler 加 --profile chrome-trace 发现某个第三方 SDK 的 AAR 里塞了 200MB 的未压缩 .so 文件,merge task 在 IO 上卡了 40 秒。这问题用普通 build scan 看不出来,因为 task 级别粒度太粗。


更激进的是 Android 团队自己开源的 androidx 构建工具链。他们在 GitHub 上放了一套基于 gradle-profiler 的宏基准测试(macrobenchmark)框架,专门测 AGP 版本升级带来的构建回归。2024 年初有个 PR(androidx/androidx#6412 附近)把他们的 Gradle 版本从 8.2 升到 8.5,宏基准测试直接抓出 compileDebugKotlin 在 K2 编译器模式下慢了 12%,回滚还是继续升级的数据一目了然。这种"用测试防退化"的思路,比事后拿 profiler 救火更根本。


Remote Build Cache 的平民化尝试


Build Cache 本地化的效果天花板很明显——你本地命中了,CI 服务器呢?新入职同事的机器呢?Remote Build Cache 的概念不新,但部署成本一直是拦路虎。Gradle 官方推荐搭个 Hazelcast 或自定义后端,中小团队一听就劝退。


2023 年下半年开始,几个云厂商的动作让这事有了新可能。GitHub Actions 的 cache backend 开始支持跨 workflow 的 Gradle 共享缓存,虽然还是基于 Actions Cache 的存储限制(默认 10GB),但对个人开源项目和小团队够用了。更实质的是 GitLab 在 16.x 版本里引入的 distributed cache 架构,把 Maven/Gradle 的远程缓存做成了 first-class citizen,配置几行 YAML 就能接入。


国内这边,阿里云效和腾讯云 CODING 也跟进类似能力,但坦白说体验参差不齐。我试过云效的 Gradle 远程缓存,配置文档里写的是"自动识别",实际要手动改 gradle.properties 里的几个内部域名,而且缓存 key 的生成策略和 Gradle 官方不兼容,clean build 的缓存命中率惨不忍睹。这种"有功能但不好用"的状态,典型反映了国内云厂商对开发者工具链的理解还停留在"功能清单打勾"阶段。


真正让我意外的是 JetBrains Space 的尝试。他们在 2024 年初把远程构建缓存做成了 Space 托管仓库的附属功能,和 TeamCity CI 深度集成。原理不神秘——就是标准的 Gradle Build Cache 协议实现,但一键开启、自动扩缩容、按存储量计费,省去了自己运维 Hazelcast 集群的麻烦。我帮一个 50 人规模的 Kotlin Multiplatform 团队评估过,全量开启后 CI 的 average build time 从 14 分钟降到 6 分钟,缓存命中率 73%。代价是每月多付几百美元存储费,ROI 算得过来。


这个路径的启示是:构建性能优化正在从"开发者自己调参数"转向"平台托管服务"。类似当年从 self-hosted Jenkins 到 GitHub Actions 的迁移,基础设施的门槛降低会释放大量被浪费的时间。


Configuration Cache 的曲折前进


必须专门吐槽一下 Configuration Cache。这个功能在 Gradle 6.6 作为实验特性引入,承诺把 configuration phase 的耗时通过序列化缓存砍掉大半。听起来完美,实际踩坑无数。


AGP 7.x 时代,打开 org.gradle.configuration-cache=true 后,十个项目里有八个会报 "invocation of Task.project at execution time is unsupported"。很多第三方插件根本没适配,而 Android Gradle Plugin 自己也是到 8.1 才算基本稳定。我 2022 年在一个商业项目里强开过,结果 lint task 各种 NPE,debug 半天发现是 AGP 的 lint model 序列化有 bug,issue 拖了三个 minor 版本才修。


Gradle 8.5 的 release note 里有个容易被忽略的点:Configuration Cache 现在默认开启了对 buildSrc 变更的增量检测。以前改一行 buildSrc 里的 Kotlin 代码,整个 configuration cache 就作废重跑,现在能只失效受影响的部分。这个改进对还在用 buildSrc 管理插件依赖的老项目意义重大——我知道很多团队迁移到 composite build 有历史包袱,buildSrc 的 performance trap 能缓一点是一点。


但 Configuration Cache 的根本问题没解决:它缓存的是"配置结果",而现代 Android 构建的配置阶段越来越重。Dynamic feature、Asset pack、Privacy sandbox SDK 这些新特性都在往 configuration 里塞东西。Google I/O 2024 上 AGP 团队演示的 "configuration phase 耗时分布" 里,解析 Android manifest 和 resources 的变体组合已经占了 40% 以上。这是 Gradle 架构层面的瓶颈,不是加个 cache 能根治的。


Bazel 的阴影和 Gradle 的防守


聊 Gradle 构建优化,绕不开 Bazel 的比较。Google 内部用 Bazel 构建 Android 源码(AOSP 本身),Meta 也大规模迁移到 Buck2/Bazel 系,社区里"Gradle 太慢换 Bazel"的声音每隔几个月就冒一波。


我的看法是:Bazel 的增量构建和远程执行(remote execution)确实在超大规模场景有理论优势,但迁移成本被严重低估。Bazel 的 Android 规则(rules_android)成熟度远不如 Java/C++,NDK 支持、资源处理、签名流程这些边缘场景坑深不见底。2023 年有个高调的案例,某海外社交 App 团队发博客讲 Bazel 迁移成功经验,细看发现他们花了 18 个月、专门养了一个 5 人工具链团队,而且最终是 Bazel 和 Gradle 双轨并行——新模块用 Bazel,遗留模块还是 Gradle。这种"成功"的性价比,绝大多数团队复制不了。


Gradle 的防守策略很聪明:不跟 Bazel 拼架构先进性,而是在现有生态里把体验磨到极致。Gradle 8.6 引入的 "project isolation" 实验特性,本质上是在不打破现有 DSL 的前提下,往 Bazel 式的 hermetic build 靠拢。每个子项目的配置可以并行、缓存可以隔离,但开发者还是写 build.gradle.kts,不用学 Starlark。如果 project isolation 能在 Gradle 9.x 稳定,Bazel 迁移的一个核心动力就被消解了。


Kotlin K2 编译器的连锁反应


回到 Kotlin 本身。Kotlin 2.0 的 K2 编译器在 2024 年 GA,这事的直接影响是编译速度,间接影响是 Gradle 构建生态的连锁优化。


K2 的前端重新实现让 compileKotlin task 在某些场景下快了 30-50%,但 Gradle 构建不只是编译。Kotlin DSL 的脚本编译、Gradle Plugin 的 Kotlin 实现、KSP(Kotlin Symbol Processing)的代码生成,这些环节对编译器前端的依赖很深。Kotlin 2.0 发布后的几个月里,Gradle 社区经历了典型的"大版本适配阵痛":KSP 2.0 的 API 不兼容导致大量处理器(processor)升级滞后,AGP 8.3 对 K2 的支持标为 experimental,JetBrains 和 Google 的协作文档散落在各个 issue 里。


我 4 月份尝试把一个用了 Room、Dagger、Moshi 的项目全量升级到 Kotlin 2.0 + KSP2,结果卡在 Dagger 的 KSP 适配上。Google 的 Dagger 团队直到 5 月底的 2.52 版本才正式支持 KSP2,之前只能用 snapshot。这种"编译器升级等插件、插件升级等编译器"的循环,是 Kotlin 生态的老毛病了。JetBrains 推新版本的节奏激进,Google 的 AGP 和第三方库的跟进永远慢半拍,开发者夹在中间当测试员。


但 K2 的一个长期利好是 Gradle Kotlin DSL 的脚本编译。K2 的 lighter tree 结构让脚本缓存更小、反序列化更快,Gradle 8.7 开始实验性支持用 K2 编译 *.gradle.kts。我测过一个 500 行级别的复杂 build script,K2 编译比 K1 快了约 20%,虽然绝对值只是几百毫秒,但积少成多。这个优化方向是 Groovy DSL 永远做不到的——Groovy 编译器没有同等力度的重构投入。


国内社区的特殊语境


写这些技术动态,得承认有个视角偏差:我主要跟踪的是 JetBrains、Gradle Inc、Google 的官方渠道和英文社区讨论。国内 Android 开发者面临的构建问题,有些是全球共性的,有些有特殊性。


一个典型差异是网络环境对依赖解析的影响。Maven Central、Google 的 Maven 仓库在国内访问不稳定,很多团队花大量时间维护私有 Nexus/Artifactory 镜像。这不是 Gradle 本身的问题,但严重放大了"构建慢"的感知。阿里云 Maven 镜像的同步延迟、jcenter 关闭后的迁移混乱,这些历史包袱至今没完全消化。你优化了 task 执行效率,但 download 阶段卡在 10KB/s,整体体验还是崩。


另一个差异是模块化实践的成熟度。国内大厂 App 的代码规模普遍偏大,微信、抖音这类超级 App 的模块数上千,但模块化拆分的技术债很重。很多项目的 settings.gradle 里用脚本动态 include 模块,配置阶段就要遍历文件系统、读 git 状态、解析自定义的模块配置 DSL。这种灵活性是以 configuration time 为代价的,而 Gradle 的优化特性(Configuration Cache、Project Isolation)对这种动态配置支持很差。官方推荐的是静态声明、显式依赖图,但迁移成本……你懂的。


我在几个技术群里观察到的现象是:国内团队对 Gradle 新版本的态度偏保守。AGP 8.x 的 API 变更、JDK 17 强制要求、DSL 语法调整,这些升级阻力让很多项目卡在 AGP 7.x 甚至 6.x。而 Configuration Cache、Build Cache 这些优化特性,版本越新效果越好,老版本要么没有要么 bug 多。这种"想优化但升不上去"的困境,比技术选型本身更磨人。


那些还没被充分讨论的方向


说完已有的进展,提两个我觉得被低估的方向。


第一个是 Gradle 的 Worker API 和 classloader 隔离的精细化。Android 构建里有大量 IO 密集型 task(dexing、merging、compressing),这些 task 理论上可以高度并行,但 Gradle 的默认并行度受限于 worker 数量和内存。Gradle 8.x 引入的 max-workers 动态调整、worker 的 classloader 缓存复用,在文档里一笔带过,实际调参空间很大。我试过把一个 20 模块项目的 org.gradle.workers.max 从默认的 CPU core 数调到 2 倍,配合 32GB 内存的机器,clean build 快了 25%,但内存峰值飙到 28GB。这种"内存换时间"的 tradeoff,CI 环境(尤其是容器化的 GitHub Actions/GitLab Runner)往往做不了,因为内存配额是硬限制。


第二个是 Android 构建对增量处理的重新定义。传统增量构建看的是"输入文件是否变化",但现代 Android 开发里,很多变化是隐式的:R8/ProGuard 的规则文件改一行,所有 dexing 都要重跑;resources 里加一张图,所有依赖这个 module 的 variant 都可能受影响。AGP 8.2 开始实验的 "incremental dexing for library projects" 和 "incremental resource processing",本质上是在重新定义"增量"的粒度。不是文件级,而是语义级——哪些变更真正影响下游的编译产物。这个方向如果做透了,比现在修修补补的 task-level 增量更有想象空间。


一个具体案例的复盘


为了把上面的抽象讨论落地,说一个我年初帮朋友团队排查的真实案例。


项目背景:Kotlin Multiplatform + Compose Multiplatform,共享代码在 shared module,Android app 在 androidApp,还有 Desktop 和 iOS 目标。Gradle 8.2,Kotlin 1.9.20,AGP 8.1。问题:每次改一行 shared 里的 commonMain Kotlin 代码,Android 的 incremental build 要 90 秒以上。


排查过程:先用 gradle-profiler 扫 --scan,发现 compileDebugKotlinAndroid 本身只 8 秒,但前面 syncComposeResourcesForAndroidgenerateComposeResClass 两个 task 各占了 30 秒+。看 task 的 input snapshot,Compose 的 resource 处理在扫描整个 shared/src/commonMain/resources 目录,而那个目录下有几百张图,每次不管代码改没改图,都要重新算 hash。


根因:Compose Multiplatform 的 Gradle Plugin 在 1.5.x 版本的资源处理逻辑有缺陷,没有 fine-grained input 声明,导致 common code change 触发了 resource task 的全量重跑。升级到 Compose Plugin 1.6.0 后问题解决,incremental build 降到 12 秒。


这个案例的启示:构建优化的瓶颈经常不在"编译"本身,而在周边 task 的 input/output 声明不精确。Compose Multiplatform 作为相对新的技术,Gradle Plugin 的成熟度远不如传统 Android 构建,这种坑会长期存在。JetBrains 的插件质量和 Google 的 AGP 相比,测试覆盖和边缘场景处理确实有差距,这是选型时要掂量的。


回到标题:新解法到底新在哪?


梳理完这些碎片,可以回答开头的问题了。所谓"新解法",不是某个银弹技术,而是几个趋势的交汇:


Kotlin DSL 的性能拐点让脚本层面的优化有了可持续的基础;远程构建缓存的托管化降低了团队接入门槛;K2 编译器和 Gradle 的协同演进把"编译脚本"这个历史包袱逐步甩掉;可观测性工具的成熟让性能回归能被自动化测试捕获。这些加在一起,构成了一个比"调参数、升版本"更系统化的优化空间。


但这个"新"也有局限。Gradle 的架构债务(configuration phase 的固有重量、Groovy 兼容的历史包袱、Plugin 生态的碎片化)没有根本解决,只是在现有框架内修得更精致。超大规模场景下 Bazel 的理论优势还在,Gradle 的防守是"够用就好"而非"全面超越"。


我个人对近期的判断是:对于 90% 的 Android 团队,Gradle 8.x + Kotlin DSL + 合理配置的缓存策略,已经能把构建耗时控制在一个不折磨人的区间。剩下的 10% 超级工程,该考虑模块化拆仓库或者 Bazel 迁移的,也别在 Gradle 上死磕了。


一个没想清楚的问题


写到这里,有个矛盾我没想透。Gradle 越来越往"声明式、静态可分析"的方向走(Kotlin DSL、Project Isolation、Configuration Cache),但 Android 构建的实际需求越来越动态(Dynamic Delivery、Feature Module 的条件交付、Privacy Sandbox 的运行时加载)。这两股趋势的拉力,最终会不会让 Gradle 的优化天花板撞得更响?还是说 Google 会在 AGP 层找到折中,把动态性封装在 Gradle 的静态框架里?


AGP 9.0 的路线图里提到了 "declarative Android Gradle plugin" 的实验,语法更接近 Bazel 的显式声明,但底层还是 Gradle。这个方向的成败,可能比任何单一优化特性都更能定义未来五年 Android 构建的体验。


等第一个尝鲜的勇士发博客吧。

Dynamic Feature Module 上线一年后的真实体验 2026-05-28

评论区