Gradle构建失败:版本兼容性、依赖冲突、JVM与Daemon排查

Gradle 构建失败,算是 CI 流水线上最常见的“绊脚石”了。我个人做了这么多年 Android CI,几乎每个项目都会遇到那么几次。说白了,大部分问题就集中在四个地方:版本兼容性、依赖冲突、JVM 内存不够、Daemon 崩溃。嗯,今天我们就聊聊这四个“老熟人”。

Gradle 构建失败排查框架 Gradle 构建失败 版本兼容性 Gradle/AGP/插件 升级前查兼容矩阵 依赖冲突 传递依赖/版本歧义 dependencyInsight 排查 JVM 内存不足 OOM / GC 频繁 gradle.properties 调优 Daemon 崩溃 进程被杀/配置冲突 --no-daemon 临时绕过 日志 + --stacktrace 定位根因 提示:多数构建失败可以通过查看 Gradle 日志的第一行异常和最后一行堆栈解决

1. Gradle 版本兼容性

这个问题我印象特别深。有一次升级 AGP(Android Gradle Plugin)从 4.2 直接跳到 7.0,结果 CI 上全是 Unsupported class file major version 的错误。为啥?因为我用的 Gradle 7.0 不配 AGP 7.0 的「官方匹配」——AGP 7.0 要求 Gradle 7.0+,但我的 JDK 版本没跟上。后来查官方兼容性表才发现,AGP 和 Gradle 版本是严格绑定的。

AGP 版本最低 Gradle 版本推荐 JDK 版本
7.07.011+
7.17.111+
7.27.311+
7.37.411+
8.08.017+

排查方式其实很简单:看构建日志最前面有没有 Unsupported Gradle 或 This version of Gradle requires 的提示。另外,我习惯在 gradle-wrapper.properties 里固定版本号,避免 CI 环境和本地不一致。

# gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
小技巧:升级 AGP 或 Gradle 前,先看一眼 官方兼容性矩阵。我一般在本地先跑一遍 ./gradlew build --dry-run 确认不出错再提交。

2. 依赖冲突

依赖冲突太常见了。项目大了以后,多个模块引入同一库的不同版本,Gradle 就会报 Conflict with dependency。我记得有一次线上用了 OkHttp 3.x 和 Retrofit 2.x 自带的 OkHttp 4.x 打架,直接导致运行时 NoSuchMethodError。CI 能编译通过,但一运行就崩。

排查依赖冲突,我推荐用这俩命令:

# 查看所有依赖树,找出冲突
./gradlew :app:dependencies --configuration debugRuntimeClasspath

# 针对某个具体依赖深入排查
./gradlew :app:dependencyInsight --dependency okhttp --configuration debugRuntimeClasspath

比方说,想看看 OkHttp 到底被哪些库引入了,用 dependencyInsight 后你会看到类似:

com.squareup.okhttp3:okhttp:4.9.0 (by 2 rules)
   variant "compile" [
      com.squareup.retrofit2:retrofit:2.9.0 (requires okhttp 4.9.0)
      com.squareup.okhttp3:logging-interceptor:3.12.0 (requires okhttp 3.12.0)
   ]

怎么解决?我在 build.gradle 里用 force 或 exclude 统一版本:

configurations.all {
    resolutionStrategy {
        // 强制使用统一版本
        force 'com.squareup.okhttp3:okhttp:4.9.0'
        // 或者排除某个传递依赖
        exclude group: 'com.squareup.okhttp3', module: 'logging-interceptor'
    }
}
避坑指南:我曾经在 CI 上直接加 force 后不测试,结果运行时还是崩了——因为 force 只保证编译期统一,运行时如果类路径里还有旧版本(比如其他 module 的 aar 里打包了),仍然会出问题。所以最好用 strictly 或直接用 BOM 统一管理。

3. JVM 内存不足

CI 机器上跑构建,最烦的就是 OutOfMemoryError 或 GC overhead limit exceeded。为啥会这样?说白了,Gradle 默认的 JVM 参数是按普通开发机器设的,CI 上如果同时跑多个任务,堆内存很容易撑爆。

常见症状:

  • 构建中途卡死,最后打印 java.lang.OutOfMemoryError: Java heap space
  • 构建时间突然变长(因为 GC 频繁)
  • Daemon 进程被系统 OOM Killer 杀掉

排查方法:

  1. 查看 CI 日志里有没有 Java heap space 或 PermGen 字眼。
  2. 在 CI 上显式指定 Gradle JVM 参数。我在 gradle.properties 里这样写:
# gradle.properties
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError

如果是 Docker 容器里跑 CI,还要注意容器自身的内存限制。我遇到过 Docker 分配 2G,但 Gradle 申请 4G —— 系统直接杀进程。

我个人的经验:CI 上不要吝啬内存。对于大型项目(100+ 模块),建议给 Gradle Daemon 分配至少 4G,容器内存至少 6G。如果还 OOM,可以试试并行编译时减少 workers:
org.gradle.parallel=true
org.gradle.workers.max=2

4. Daemon 崩溃

Daemon 崩溃一般发生在 CI 环境不稳定的时候。比如构建到一半,日志里突然出现 Daemon is shutting down 或者 Gradle Daemon disappeared。为啥?常见原因:

  • CI 节点内存压力大,系统杀掉了 Gradle 的守护进程
  • Gradle 缓存损坏
  • Daemon 版本和当前 Gradle 版本不匹配(比如 CI 上同时跑不同版本的 Gradle 任务)

排查步骤:

  1. 先看日志末尾有没有 Process 'Gradle build daemon' finished with non-zero exit value。
  2. 如果看到 InvalidDaemonInstallationException,大概率是缓存问题。
  3. 临时解决方案:加上 --no-daemon 重新跑一次,绕过 Daemon 直接构建。
./gradlew clean build --no-daemon

注意:--no-daemon 会让每次构建启动一个新 JVM,速度会慢 30% 左右,只适合临时绕过。

我的习惯:在 CI 上,我会显式设置 org.gradle.daemon.idletimeout=600000(10 分钟空闲超时),避免 Daemon 长时间占用内存。同时确保 CI 节点没有其他资源密集型任务和 Gradle 抢内存。

综合排查流程

最后,给大家画个总结。遇到 Gradle 构建失败,别慌,按这个顺序检查:

  1. 看日志头部:版本不兼容的信息通常出现在前 10 行。
  2. 看依赖树: ./gradlew dependencies 跑一遍,找冲突。
  3. 看 JVM 参数: gradle.properties 里的内存设置。
  4. 看 Daemon 状态: ./gradlew --status 确认守护进程是否正常。
  5. 加 --stacktrace: 重新跑一次,拿到完整堆栈。

嗯,这四个问题覆盖了 90% 的 Gradle 构建失败场景。剩下的 10% 可能是插件 bug、网络超时等,但只要有日志和 stacktrace,总能找到根因。记住,日志才是你最好的朋友。