ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android Gradle下载编译失败排查与解决指南:版本匹配、镜像源与缓存清理实战

Android Gradle下载编译失败排查与解决指南:版本匹配、镜像源与缓存清理实战 1. 先搞清楚Android Gradle 编译失败究竟挂在哪个环节做 Android 开发这些年我至少有一半的加班时间不是贡献给业务逻辑而是花在了一行行 Gradle 报错上。这个项目标题写的是“Android Gradle 项目下载编译失败解决”还标注了“持续更新”我特别认同这个定位因为 Gradle 的坑真的不是一次性踩完就结束的。今天这篇就是把我长期维护 Gradle 下载与编译问题排查笔记后的成果整理出来适合被 distributionUrl 卡住的人、被 Running Gradle task 折磨的人以及刚从 IDEA 或 Android Studio 转过来、还不熟悉整套构建体系的新手参考。说句实话绝大多数 Gradle 编译失败根本不是你代码写得有问题而是构建链路中的某一环没对齐。只要你能准确判断它属于哪一类排查范围立刻就能缩小一大半。很多同学一看到红色报错就开始复制粘贴到搜索引擎一条条试结果越搞越乱原因就是没先做故障分类。1.1 三种失败形态先分清再动手我遇到的所有失败粗分起来只有三类下载失败、依赖解析失败、编译执行失败。别看报错五花八门本质上逃不出这三种。下载失败的典型症状是卡在下载进度条半天不动或者日志里一直出现 Downloading 的 URL 路径然后就超时了。这种问题最常出现在第一次创建项目或换电脑同步代码时因为 Gradle 发行包、Android Gradle PluginAGP、各种依赖库都还没进本地缓存。症状明显定位也最快。依赖解析失败的表现是 Could not find ... 或 Failed to resolve: ...。这类报错看着像是在说“找不到某个包”实际上往往不是包不存在而是你的仓库源列表里没有那个仓库、仓库地址访问不了或者版本号写错。热词里经常出现的 “Could not resolve all files for configuration”基本都属于这一类。编译执行阶段抛出的错误就五花八门了比如 AGP 版本和 Gradle 版本不匹配、namespace 缺失、Java 版本对不上、Kotlin 编译内存溢出等。这类报错一般会直接告诉你哪个文件哪一行出问题反而是最好解决的。1.2 我自己的排错顺序我的排错顺序非常简单粗暴就五句话先网络后代码先版本后语法先环境后项目先缓存后依赖先命令行后 IDE。先网络后代码的意思是看到编译失败先不要急着看自己逻辑哪里写错了先确认是不是有个资源下载不下来。因为 Gradle 的报错链很长如果底层依赖没下载成功上层会冒出一堆莫名其妙的问题。先版本后语法是优先检查 AGP、Gradle、JDK 三者的版本是否匹配不匹配时抛出的错误往往让人误以为是代码问题。先环境后项目是说先把全局的 Gradle 配置、SDK 路径、JDK 版本确认好再进项目看 build.gradle。先缓存后依赖是考虑本地缓存的旧版本或者残缺文件会不会干扰构建。最后才是回到命令行复现不要只在 IDE 里点按钮因为 IDE 有时候会把真正的错误信息给吞掉。这个顺序帮我省了大量时间。以前我遇到报错就进 build.gradle 里改配置改来改去发现根本没到那一步浪费了整整一个下午。后来学乖了老老实实按顺序查大多数问题在第一步、第二步就能锁定。2. 下载慢、卡在下载进度条换源、离线包、版本匹配三板斧下载类问题应该是中文开发者社区里被问得最多的也是我整理“持续更新”清单时唯一一个反复增加内容的板块。究其原因一是 Gradle 发行包默认从国外服务器下载二是 Maven 仓库默认地址在国内访问速度不稳定三是很多人压根不知道有一个叫 gradle-wrapper.properties 的文件在控制这一切。2.1 distributionUrl 和仓库源两个容易忽略的配置点每个 Gradle 项目里都有一个gradle/wrapper/gradle-wrapper.properties文件它就是整个构建链路的入口。里面真正起决定作用的就是distributionUrl它告诉 Gradle Wrapper 该去哪个地址下载对应版本的 Gradle 发行包。默认内容长这样distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists distributionUrlhttps\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBaseGRADLE_USER_HOME zipStorePathwrapper/dists问题就出在services.gradle.org上。这个地址不是不能用而是很多网络环境下下载速度极其不稳定经常下载到一半就断断了下一次从头再来于是你就看到进度条一直卡在某个百分比。我的做法是把 distributionUrl 换成国内镜像地址distributionUrlhttps\://mirrors.cloud.tencent.com/gradle/gradle-8.0-bin.zip腾讯云、华为云、阿里云都有 Gradle 发行包镜像。以腾讯云为例路径规则相当简单把services.gradle.org/distributions整段替换成mirrors.cloud.tencent.com/gradle就行。改完之后删掉项目里的.gradle目录重新同步下载速度肉眼可见地提升。然后是仓库源。新建项目时settings.gradle 里默认配置的是 google() 和 mavenCentral()这两个源在部分网络环境下也不够快。我常用的方案是在 settings.gradle 里把仓库地址显式写成阿里云镜像pluginManagement { repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/gradle-plugin } gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/gradle-plugin } } }有人会问为什么不直接删掉 google() 和 mavenCentral()只留阿里云我建议保留它们作为兜底因为镜像仓库理论上存在同步延迟冷门库和新发布的版本可能偶尔没有。保留官方仓库地址不会影响速度Gradle 是逐个仓库去找依赖的找到就会停止前几个仓库能命中就不会轮到后面的。2.2 离线包的正确使用姿势离线包这词听着简单但很多同学栽在“我把 zip 下载下来了项目还是下载失败”上。原因很简单Gradle Wrapper 不是随便看到 zip 就会用它有一套自己的缓存目录结构和校验逻辑。正确做法分两种情况。第一种是给一台完全没网或者网速极差的机器用最稳的办法是找一台已经能正常构建的同配置机器把~/.gradle/wrapper/dists整个目录拷贝过去放到离线机器的相同路径下。这个目录里不光有 zip还有已经解压出来的 Gradle 发行版而且目录结构带上了 hash 后缀Gradle 是靠这个路径来定位缓存的。直接拷贝能保证路径完全匹配基本不会出幺蛾子。第二种是网络环境尚可、但不想每次重复下载的场景。可以把 distributionUrl 改成指向本地文件distributionUrlfile\:///D:/gradle-dist/gradle-8.0-bin.zip注意这个方案有个坑。如果distributionUrl原本的地址带 hash 校验信息Gradle 下载后校验不通过照样报错。所以改成 file 协议时要确保 zip 是官方原包没有改动过。另外文件路径里的正斜杠、反斜杠和中文目录一定要处理干净否则会解析失败。依赖库的离线也一样不是把 jar 下载了丢给项目就行。靠谱做法是把在线环境的~/.gradle/caches/modules-2/files-2.1目录拷贝到离线机器对应位置或者直接把整个~/.gradle/caches拷贝过去。这样离线构建时会优先命中本地缓存不需要重新下载。我实测过这种整目录迁移比零散地往项目里塞 jar 可靠得多。2.3 AGP、Gradle、JDK 版本到底怎么配版本不匹配这个问题几乎每周都有同事来问。症状多种多样比如编译时提示 “Minimum supported Gradle version is X”或者直接出现 Unsupported class file major version还有的报错信息完全看不懂其实根源都是三个东西没对齐AGP、Gradle、JDK。我把 Android 开发常用的兼容组合整理一下AGP 版本最低 Gradle 版本推荐 JDK 版本AGP 7.4Gradle 7.5JDK 11AGP 8.0Gradle 8.0JDK 17AGP 8.1Gradle 8.0JDK 17AGP 8.2Gradle 8.2JDK 17AGP 8.5Gradle 8.7JDK 17这张表记住一个硬规则AGP 8.0 以上JDK 17 是强制要求。哪怕你 Gradle 版本对了IDE 里默认的 JBR 是 11编译照样过不去。很多同学项目从 AGP 7 升级到 8 之后报一堆看不懂的错十有八九是 JDK 还没切。版本匹配的优先级要看这个顺序先确定 AGP 版本再去定 Gradle 版本最后看 JDK。AGP 由你的项目需求决定Gradle 跟随 AGPJDK 再跟随 Gradle。实际项目里gradle-wrapper.properties 里的 Gradle 版本决定了你在命令行里编译用哪个版本而 IDE 里的 Gradle JDK 设置则决定运行环境。两个地方都对了编译链路才算通了一半。另外AGP 8.0 之后强制要求模块的 build.gradle 里显式声明 namespace不能再依赖旧版的包名推断。如果你升级到 AGP 8 后报错 “namespace is not specified”直接去模块的 build.gradle 加上android { namespace com.example.app }这类报错特别迷惑人因为代码本身没任何问题纯粹是 AGP 新版本对工程结构的规范要求。顺手记住能少走很多弯路。3. 编译阶段的疑难杂症卡死、内存、缓存还有那些“非典型”报错下载问题解决了编译阶段又会冒出一批新麻烦。最常见的就是 Android Studio 底部一直显示 “Running Gradle task assembleDebug...”转圈转到地老天荒或者编译进行到一半直接 OutOfMemory再比如清理缓存后精心配置的构建环境一夜回到解放前。这几类问题不搞清楚原理只能反复试错。3.1 assembleDebug 卡住不动先看 Daemon 和 JVM 内存“Running Gradle task assembleDebug...” 卡很久第一反应不应该是骂电脑卡而是想想 Gradle 的守护进程Daemon是怎么跑的。Gradle 构建会启动一个常驻 JVM 进程所有任务都在这个进程里执行。如果这个 JVM 堆内存设置太小构建任务稍重一点就会频繁触发 GC呈现出来的现象就是这个任务执行了十分钟还没结束CPU 倒是跑满了。我见过太多项目压根没配置过 gradle.properties 里 JVM 参数用的是默认值。默认的-Xmx往往只有几百 MB 到 1GB对现代项目来说真的很紧张。我自己维护的项目 gradle.properties 里长期放着这么一段org.gradle.jvmargs-Xmx2048m -XX:MaxMetaspaceSize512m -XX:HeapDumpOnOutOfMemoryError org.gradle.paralleltrue org.gradle.cachingtrue org.gradle.daemontrue-Xmx 设到 2G 是一个平衡点够大多数项目用又不会挤占系统内存。如果项目特别大、模块特别多可以考虑 3G 或 4G但不要无脑调高机器物理内存不够反而会用到交换分区直接在系统层面拖垮构建。org.gradle.paralleltrue 是让各个模块并行构建多模块项目受益最明显。org.gradle.cachingtrue 会启用构建缓存同一个 task 的输入没变化时可以直接复用之前的输出这对重复构建省时间非常有效。改完这些参数后必须重启 Daemon这个点很多人会忽略因为旧的 Daemon 进程还保留着旧参数你改了 gradle.properties 它不知道感觉怎么改都没用。具体操作后面第 4 节会讲。3.2 清理缓存的正确时机和正确位置Gradle 的缓存分三层项目目录下的.gradle、全局~/.gradle/caches以及~/.gradle/wrapper/dists。很多人一遇到编译问题就rm -rf ~/.gradle这是最粗暴但最不可取的做法因为它把发行包、依赖缓存、构建缓存全删了下次构建会陷入漫长的重新下载而且离线包也白准备了。什么情况下才需要清理缓存我总结出三个时机。第一你改了仓库源地址但旧的依赖缓存里存在同名不同内容的 jar这时候需要清掉~/.gradle/caches/modules-2下对应 group 的目录再刷新。第二某次下载因为断网中断留下了残缺文件后续构建反复报错说文件校验和不对这时候删除对应缓存即可。第三Gradle 大版本升级后插件缓存不兼容会出现各种诡异异常这种情况清缓存是性价比最高的选择。日常优先清理项目目录下的.gradle文件夹。它是每个项目私有的构建状态删了不影响全局缓存重新同步就会重新生成。只删它的成本最低解决大部分状态错乱问题。全局缓存能不动就不动因为那是你花了很长时间积累下来的财富。还有一个容易被忽略的位置~/.gradle/daemon。如果 Daemon 日志显示 OutOfMemory或者反复启动失败可以把这个目录下的日志翻出来看一眼确认是 JVM 参数问题还是缓存损坏。看到日志里有OutOfMemoryError关键字就别急着清缓存了先调内存。3.3 非典型场景Flutter 模板报错和 CEF 这类 native 编译热词里有两个特别有意思的报错虽然不是纯 Android 项目但查来查去根子都在 Gradle 上。先说 Flutter 项目的报错You are applying Flutters main Gradle plugin imperatively using the apply script.这个报错我一开始也看懵了因为 Flutter 项目的 android 目录本质上是 Android Gradle 工程构建链路也是由 Gradle 承载的。出现这个错基本可以判断是项目里的android/settings.gradle还在用旧版 Flutter 模板的写法比如通过apply from: $flutterRoot/packages/flutter_tools/gradle/app_plugin_loader.gradle这种方式引入 Flutter Gradle 插件。新版 Flutter 已经把插件声明方式改成了常规 Gradle 插件声明old 写法被判定为“imperative”不允许再用了。修复方法并不复杂但我不建议手改最省心的是用当前版本的 Flutter SDK 重新生成一个新项目把 lib 目录和 pubspec.yaml 拷过去android 目录直接换新。如果你非要手改思路是把 settings.gradle 里的旧 apply 脚本移除改成新版模板里的 includeBuild 指向同时在 app/build.gradle 里用 plugins 块声明id dev.flutter.flutter-gradle-plugin。我试过手改遇到过一次 pluginManagement 配置和 Gradle 版本之间的小坑最后还是换成新模板省事。再说 “编译 CEF 失败”。CEF 是 Chromium Embedded Framework常见于桌面端应用和 Android 没有直接关系但它的报错逻辑和 Gradle 下载失败完全同构构建时需要从一个远程地址拉取预编译二进制包下载失败或者文件不完整整个 native 编译就挂了。遇到这种问题别去修改编译参数死磕先确认那个依赖包的下载环节是否成功。我在本地没有现成镜像的情况下会把 CEF 的二进制手动下载下来放到项目指定的缓存目录再配置 CMake 或构建脚本指向本地路径。道理和 Gradle 离线包完全一样提前把产物放到它该在的地方构建链路就顺了。4. 命令行与 IDE 里最常用的三个排错动作很多同学习惯在 Android Studio 的弹窗里看错误红色提示信息经常是截断过的真正有用的 Caused by 藏在 Build 窗口的上层日志里。这时候命令行工具反而是最好的朋友。用命令行的另一个好处是能在没有图形界面的服务器或 CI 环境里复现问题也能让你更清楚地掌握 Gradle 的执行链路。4.1 --stacktrace、--info把“看不见的错误”变成“看得见的错误”我排错的第一命令永远是带--stacktrace的构建命令./gradlew assembleDebug --stacktrace这个参数会让 Gradle 在构建失败时输出完整堆栈把真正抛异常的那一行代码亮出来。默认情况下 Gradle 只显示一个缩写后的错误摘要经常让人摸不着头脑。加了 stacktrace 后至少能看清楚是哪个任务、哪个类、哪一行出了问题。如果 stacktrace 还不够那就上--info./gradlew assembleDebug --info --stacktrace--info 会输出每个依赖的解析过程、每个任务的执行状态、下载了哪些文件信息量大到爆炸。第一次跑的时候你可能会被日志淹没但没关系我们的目标不是读完所有日志而是搜索关键字。我习惯配合 grep 用./gradlew assembleDebug --info --stacktrace 21 | grep -A 20 Caused by-Caused by 后面的内容通常才是真正的病因。只看一行报错很容易被表面信息误导比如明明是依赖下载 404前面却先冒出一堆找不到类的编译错误。顺着 Caused by 一层一层翻能翻到最底层的根因。还有个小技巧构建的时候顺手加--warning-mode all它会把你项目里的所有废弃 API 警告、过时配置都列出来。这种警告平时不影响构建但一旦升级 AGP 或者 Gradle它们往往会变成硬错误。提前看到提前处理。4.2 Daemon 管理与离线模式--status、--stop、--offline前面反复提到要重启 Daemon这节就把命令列全。第一个是查看当前 Daemon 状态./gradlew --status它会列出所有 Gradle Daemon 进程的 PID、Java 版本、JVM 参数和运行状态。改完 gradle.properties 后你看到旧 Daemon 还活着就知道为什么参数没生效了。这时候执行./gradlew --stop全部停止后再跑构建时会自动启动一个带着新参数的新 Daemon。很多人的困惑是“我改了配置为什么没用”八成就是这一步没做。再说--offline模式。这个参数在无网环境或网络特别差的环境下是救命的./gradlew assembleDebug --offline它强制 Gradle 不访问任何远程仓库只使用本地缓存里已有的依赖和插件。如果本地缓存完整构建速度会非常快而且不会因为网络中断而失败。但如果本地缓存本来就缺东西它会直接报找不到依赖而不是去下载。所以离线模式的应用场景是你已经成功构建过一次第二次想加速或者你拷了别人的缓存目录后使用或者你明明在无网环境还想碰碰运气验证本地依赖是否完整。4.3 Android Studio / IDEA 的 Gradle 配置这三个开关最容易设错IDE 里的 Gradle 配置看着简单实际上有三个开关特别容易设错。第一个是 Gradle JDK 的选择。Android Studio 的 Settings - Build, Execution, Deployment - Build Tools - Gradle 里有一个 Gradle JDK 下拉框。AGP 8.0 以上的项目必须选 JDK 17很多人默认选的是嵌入的 JBR 11构建直接失败。我建议把它明确指到 JDK 17 的安装路径不要依赖默认值。第二个是 Gradle 发行版的来源选择。同一个设置页面里有 “Use Gradle from” 和 “Use specified location” 的选择。优先保持默认的 “Wrapper”因为 wrapper 会读取项目里 gradle-wrapper.properties 的配置这样团队协作时大家用同一个版本。如果选了本地安装路径其他人可能会因为本地 Gradle 版本和项目不匹配而构建失败。第三个是 Gradle user home 的路径。默认是~/.gradle除非有特殊原因不要随便改。多个项目共享同一个 user home 是件好事因为依赖和发行包可以复用。我曾经为了“隔离”把两个项目的 user home 分开配置结果每个项目都要重新下载一遍全量依赖磁盘和网络全遭罪。设置改完之后记得执行一次 Sync Project with Gradle Files让 IDE 重新读取配置。有时候改了 settings.gradle 里的仓库地址IDE 不会自动刷新还要手动点一下同步按钮不然会一直用旧的依赖解析结果。5. 常见报错速查表以及我踩过最久的两个坑5.1 高频报错速查表这个清单是根据我日常答疑中最高频的几类问题整理的做成速查表方便先自查。每一行都配了快速处理思路点击率最高的几个问题基本都能在这里找到答案。现象可能原因快速处理下载进度条卡住长时间不动默认 distributionUrl 访问不稳定换成腾讯云、华为云等 Gradle 镜像Could not find com.android.tools.build:gradle:X.X.X仓库源缺少对应版本或源访问不了换阿里云镜像确认 AGP 版本存在Minimum supported Gradle version is XAGP 和 Gradle 版本不匹配查兼容表升级 Gradle wrapper 版本Unsupported class file major versionJDK 版本和当前 Gradle/AGP 不匹配AGP 8 切到 JDK 17namespace is not specifiedAGP 8 不再自动推断包名模块 build.gradle 里显式声明 namespaceRunning Gradle task assembleDebug 卡很久Daemon 内存太小或首次构建下载依赖调大 gradle.properties 的 JVM 参数重启 DaemonOutOfMemoryError: GC overhead limit exceeded构建 JVM 堆内存不足增加 -Xmx并检查日志确认是哪个 Task 内存爆炸Could not resolve all files for configuration依赖解析失败仓库源或依赖坐标有误逐层检查 Caused by确认仓库源确认版本号改了仓库源但编译还是用旧依赖本地缓存干扰清理项目 .gradle必要时清 modules-2 中对应缓存这张表看着简单但每一条背后都有具体的报错场景。比如 “Could not resolve all files” 这个有一次是因为依赖坐标里的 groupId 大小写写错了一次是仓库地址多了一个末尾斜杠导致 401还有一次是公司内网只放行了部分仓库地址。所以看问题不能只看表面文案还是要往下追 Caused by。5.2 两个花掉我最多时间的坑最后分享两个我印象最深的坑每一个都浪费了我至少两个晚上希望写出来你能直接绕开。第一个坑是 distributionUrl 换完镜像之后IDE 依然显示下载失败。第一次遇到时我以为镜像地址写错了反复核对了好多遍都没发现问题后来猛然意识到IDE 里用的 Gradle Wrapper 还是旧进程缓存的旧地址修改 gradle-wrapper.properties 后必须让项目重新执行 wrapper 任务或者重启 IDE。最直接的办法是先跑一遍./gradlew --version让它用新地址把发行包拉下来或者干脆删掉~/.gradle/wrapper/dists下这个版本对应的目录再回 IDE 里 Sync。这个坑教会我一个道理 Gradle 的缓存系统“太聪明”有时候聪明过了头反而让人误判问题点在配置本身。第二个坑是命令行构建和 IDE 构建的结果不一致。明明命令行./gradlew assembleDebug成功了回到 Android Studio 点运行就失败报错内容毫无规律。查了很久才发现命令行和 IDE 各自的构建状态不一致一个用了旧缓存一个用了新缓存构建产物互相干扰。后来我统一了操作路径日常开发构建都走 IDE出现报错需要深挖时再去命令行复现而且命令行构建前先--stop关掉 Daemon再跑--stacktrace确保两个环境尽量在同一状态下对比。这个习惯保留到现在稳定性明显上去了。这个“持续更新”的清单我还会一直维护下去。以后遇到的每一种新报错我都会把现象、原因、处理过程补进这个速查表。如果你也被某个 Gradle 问题卡住过先对照上面的顺序自查一遍很多时候问题就出在版本匹配和缓存清理这两个不起眼的环节上。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进