你正兴高采烈地打开一个刚拉回来的 Android 项目,IDE 还在转圈导入,Gradle sync 的进度条就停在某个百分数不动了。几秒后,Event Log 里多了一大串红色报错,有 Could not resolve,有 Connection refused,还有 Download 卡了几分钟最后超时。这种场面我见过太多次,甚至曾经在群里看到人发一张报错截图,下面紧接着就是“怎么搞?”。其实在这个阶段,九成问题都不是你代码的问题,而是环境、网络、缓存、版本四个环节里至少有一个在拖后腿。
所以我打算把 Android Gradle 项目下载和编译失败这件事,做成一份足够系统的排错笔记。它不会只给零散答案,而是按“报错发生在哪个环节”来拆解,告诉你先看什么、再改什么、什么情况下要下重手。这篇文章会持续更新,每次我自己或身边同事又踩到新坑,我都会把排查过程补进来。适合正在被 sync 卡住的初学者,也适合 CI 上突然挂掉、对着日志一头雾水的团队同学拿来对照。
1. 别急着一路 clean:先定位编译失败发生在哪一层
1.1 报错信息里最有价值的三个字段
看到一堆报错别慌。第一步不是 clean,不是重启 IDE,而是把报错完整复制下来,从里面抓三个信息。第一个是依赖坐标,像Could not resolve com.example:library:1.0.0,告诉你具体是哪个依赖出了问题;第二个是失败任务,Execution failed for task ':app:compileDebugKotlin',告诉你在构建的哪一步挂掉;第三个是底层原因,Connect timeout、checksum mismatch、No cached version available,这直接决定了改仓库、改缓存还是改工具链。
| 报错字段 | 常见样子 | 它告诉你什么 |
|---|---|---|
| 依赖坐标 | Could not resolve com.example:library:1.0.0 | 哪个依赖拉不到 |
| 失败任务 | Execution failed for task ':app:compileDebugKotlin' | 构建在哪一步挂掉 |
| 底层原因 | Connect timeout / checksum mismatch / no cached version | 该修网络、缓存还是仓库地址 |
很多人不看底层原因,直接把整段日志发群,最后得到的答案也只是“换个镜像试试”。如果自己先把这三个字段拆出来,排查范围可以缩小一大半。另外建议把完整报错保存下来,不要只在 IDE 弹窗里看,因为弹窗可能折叠掉关键上下文,尤其是任务名和某个依赖路径。
1.2 Sync 失败和 Build 失败是两种完全不同的场景
Gradle sync 是项目导入和配置阶段,它主要做的事情是加载插件、解析依赖、构建设置。如果 sync 失败,报错往往发生在配置阶段,根因集中在仓库访问、插件坐标、SDK 路径、Gradle 版本这些地方。而 Build 失败是执行阶段,可能是 Kotlin 编译、资源合并、代码混淆等具体 task 出问题。
如果你看到 sync 成功,但真正运行 build 才爆红,那多半要检查代码和插件配置,而不是继续折腾仓库镜像。很多人把这两种场景混在一起排查,白白浪费了不少时间。举一个我见过很多次的例子:sync 成功,执行打包时 AAPT2 报错,有人立刻去换仓库源,换完问题照旧。AAPT2 报错通常是资源文件本身有语法问题,或者 AAPT2 可执行文件与当前系统/AGP 版本不匹配,跟依赖仓库没有任何关系。把问题先归类,才不会被报错表面的英文字母带偏。
1.3 一个快速的分层排查表
下面这个表是我处理问题时最常对照的。你可以把它当成第一张筛子,看到报错先按表归个类,再决定下一步动作。
| 报错特征 | 优先排查层 | 第一动作 |
|---|---|---|
| Could not resolve / Could not GET / Download 超时 | 仓库与网络 | 换镜像、检查仓库声明 |
| SDK location not found | 本机 SDK 路径 | 配置 local.properties 或环境变量 |
| Unsupported class file major version | JDK 与 Gradle 版本 | 检查 Java 版本和 Gradle 运行时 |
| Execution failed for task ':app:mergeDebugResources' | 资源文件与 AGP | 检查资源文件、AAPT2 和 AGP 版本 |
| Kotlin could not find required JDK | JDK 配置 | 给 Gradle daemon 指定正确 JDK |
这张表不能覆盖全部情况,但能覆盖大多数我实际遇到过的下载编译失败。后面的章节会展开每一项怎么查、怎么改,以及几个容易被误判的案例。
2. 依赖拉不下来?先从 Maven 仓库的“远近”开始管
2.1 卡在 Download 的本质是仓库可达性问题
Gradle 默认声明的公共仓库服务位于海外,某些网络环境下连接不稳定甚至直接超时。尤其是项目第一次 sync、本地缓存为空的时候,每个依赖都要从远端拉一遍,只要有一个连接超时,整个构建就会失败。这不是项目代码问题,是仓库可达性不好。把仓库换成国内可用的镜像源,或者公司内部搭建的私服,是最直接的解决手段。
就算你平时打开网页很顺畅,也不代表 Gradle 下载能稳定通过。浏览器有缓存、有 CDN,而 Gradle 请求依赖库的后端路径可能因为 TLS 握手、DNS 解析等因素超时。所以我常跟人说:先别怀疑代码,先怀疑到仓库之间的那段链路。判断方式也简单:在报错日志里搜Could not GET,如果后面跟的是一长串超时时间,基本就是仓库访问问题。此时再换仓库,比反复 clean 有用得多。
2.2 仓库声明顺序会影响解析速度
在 Gradle 里,依赖仓库是按声明顺序查询的。如果第一个仓库刚好是一个慢源,Gradle 会先去那边找,超时之后才轮到第二个仓库,来回几次,等待时间就非常可观。反过来,把可达性好的镜像放在最前面,大多数依赖第一次就能命中。
不过要注意,不是所有依赖在任何仓库里都存在。比如很多 Android 官方库只在 Google 仓库能拿到,你只放镜像不够。所以合理的策略是:国内镜像放第一名,google() 和 mavenCentral() 放在后面兜底。这样既保证速度,又保证覆盖。镜像往往有同步延迟,如果某个依赖刚发布、镜像里还没有,Gradle 会自动继续找下一个仓库,不会影响解析。这也解释了为什么有些人设置了镜像仍然失败,要么是仓库顺序不对,要么是声明位置不对。
2.3 配置镜像的推荐位置和正确打开方式
现代 Gradle 项目建议在 settings.gradle(.kts) 里用 dependencyResolutionManagement 统一管理仓库。比如:
// settings.gradle dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { maven { url = uri("https://mirrors.example.com/maven") } google() mavenCentral() } }这里镜像地址我给的是一个占位符,因为它经常变化,你搜索“国内 maven 仓库镜像”就能找到当前可用的地址。设置之后,所有模块都会走这套仓库配置,不用在每个 build.gradle 里重复。如果你还在用老式的 allprojects 写法,也建议迁移到这里统一管理,很多依赖解析的怪问题就是这么被避免的。
还要注意一个细节:repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)会禁止模块单独声明仓库,强制所有仓库配置都集中处理。这是好习惯,但迁移到老项目时可能触发“Repository was not found”之类的错误。遇到这种情况,需要检查各个模块里有没有自己声明的仓库,把它们挪到 settings.gradle 里统一处理,再去掉模块内的仓库代码。
2.4 还有哪些隐藏网络问题:Gradle Wrapper 下载、动态版本
除了依赖本身,Gradle 发行包也可能下载失败。打开项目时,IDE 会根据 gradle-wrapper.properties 里的 distributionUrl 去下载指定版本的 Gradle,如果这个地址访问不通,界面会一直卡在下载 Gradle 的提示上。症状跟依赖下载失败很像。解决思路有两个:一是手动把 Gradle 发行包下载到本机,修改 distributionUrl 指向本地文件;二是用一个局域网内可访问的镜像地址。注意修改之后要重启 Gradle daemon,否则旧进程还占着文件。
另一个和网络相关的坑是动态版本。有人为了省事写implementation "com.example:lib:+"或者latest.release,这样每次 sync 都要向仓库查询最新版本,仓库稍不稳定就会失败。上线项目尽量锁死具体版本号,把构建行为变成可预期的东西。动态版本在公司团队的公共模块中尤其危险,你永远不知道下一次构建拉回来的代码是什么。
3. 构建工具链的三件套:Gradle、JDK、Android SDK
3.1 版本矩阵不符会直接编造出“奇怪报错”
AGP(Android Gradle 插件)、Gradle、JDK 三者之间存在兼容性矩阵。版本不匹配时,报错往往不会直接说“你该升级”,而是变成一堆看起来很奇怪的异常。常见的对应关系大概是:AGP 7.4 一般需要 Gradle 7.5 和 JDK 11;AGP 8.1 以上建议 Gradle 8.0、JDK 17;Gradle 8.x 多数也需要 JDK 17 才能运行。如果你开了新项目却用了旧模板,报错往往看着像 Kotlin 或依赖解析问题,实际是版本矩阵不对。
检查方式很简单:在项目根目录执行./gradlew --version,看输出的 Gradle 版本和 JVM 版本,再对照你项目里声明的 AGP 版本。如果发现某个版本偏低,优先升级 Gradle Wrapper,而不是立刻降 AGP,因为降 AGP 可能引入其他不兼容。在 IDE 里开发时,建议用自带 JDK 而不是系统 JDK,减少同一台机器多项目之间的版本打架。CI 环境则单独安装 JDK 17,并确保环境变量 JAVA_HOME 指向它。
3.2 SDK 找不到时,先看一眼项目里的 local.properties
“SDK location not found”是我见过最多的配置类报错之一。项目根目录的 local.properties 是 IDE 生成的本地配置文件,里面会写一行sdk.dir=...,指向本机 Android SDK 所在目录。如果这个文件缺失、路径不对,Gradle 就完全不知道 SDK 在哪。解决方法是在项目根目录创建或修正 local.properties,或者设置环境变量 ANDROID_HOME 指向 SDK 根目录。
注意 local.properties 里是本机绝对路径,默认不应该提交到版本控制。从开源平台 clone 的项目没有这个文件是正常的,自己建一个就行。构建服务器上如果没有装 Android SDK,需要先安装命令行工具并在 CI 脚本里配置路径。还有一个容易被忽略的点:sdk.dir 指向的目录需要对当前用户有读写权限,否则下载 SDK 组件或生成构建产物时又会失败。
3.3 缓存目录能解决 90% 的“玄学失败”
当你反复遇到依赖下载失败、jar 包半途损坏、校验和始终对不上时,问题很可能出在 Gradle 本地缓存。Gradle 会把下载过的依赖缓存在~/.gradle/caches,项目构建产物在项目根目录的build/下。缓存损坏后,常见报错是checksum mismatch或者一些看起来莫名其妙的文件锁定提示。
处理步骤并不复杂:先执行./gradlew --stop把 Gradle daemon 停掉,然后删除~/.gradle/caches里对应的缓存目录,再重新 sync。不需要每次把整个缓存目录全删掉,可以先按模块删,删错了无非重新下载一次。如果实在定位不到是哪个文件损坏,才考虑移除整个 caches 目录。另外,磁盘空间不足也会让异步下载半途失败,遇到下载卡住时顺手检查一下磁盘剩余空间,能省不少时间。clean 命令主要清的是项目 build 产物,清不掉全局缓存,所以别再迷信一路 clean。
4. 几个典型的失败案例复盘,照着比对
4.1 案例 A:AGP 与 Gradle 版本不匹配,只在构建时爆红
有次我帮人看一个从旧仓库里拉下来的工程,sync 一直报“AGP requires Gradle 8.0”,但项目 gradle-wrapper.properties 里写的还是 Gradle 7.6。这种问题在团队协作中很典型:仓库里代码是新写的,AGP 版本被升级过,但 Gradle Wrapper 没有一起提交,或者有人用了本机老版本覆盖了 wrapper。解决办法很简单,编辑 gradle-wrapper.properties 里的 distributionUrl 指向需要的 Gradle 版本,重新 sync。
这里要提醒一句:不要看到 AGP 要求高,就顺手把 AGP 降回旧版。降级 AGP 可能让代码里的新特性全部报错,比如某些 API 只在 AGP 8 里可用。改版本之前最好备份项目里的 gradle 配置文件和 wrapper.properties,因为 Gradle 升级过程中会自动迁移部分配置,有时候会改掉你原本的插件声明。保持“先升级 Gradle,再升级 AGP,最后统一 JDK”的顺序,会稳很多。
4.2 案例 B:依赖全部解析成功,却在 Transform 阶段失败
另一个项目 sync 一切正常,依赖也都能解析,只有执行assembleDebug时在某个 Transform 阶段崩溃,堆栈里完全看不到业务代码。查到最后是 Kotlin 插件版本在不同模块之间不统一,一个模块用 1.8,另一个模块用 1.9,导致编译器插件链互相不兼容。这种情况盯着报错位置查很难定位,正确做法是先从根目录的版本目录文件或 build.gradle 里确认 Kotlin 版本只用了一个,再执行./gradlew dependencies看实际解析出来的版本有没有冲突。
类似的“伪编译错误”还遇到过:源码目录里突然出现两个同名类、混淆规则写错导致 R8 阶段崩溃、资源文件命名不规范触发 AAPT2 异常。它们的共同特点是 sync 正常、依赖正常,但真正执行任务的时候挂掉。遇到这类问题,不要碰仓库源,先看 task 名,再回溯对应模块的配置和代码。
4.3 案例 C:NDK 与 ABI 过滤导致安装包编译崩溃
还有一个典型的坑:项目包含原生代码,模块里写了 ABI 过滤配置,比如只保留 arm64-v8a,但本机 NDK 版本和代码预期版本不一致,链接过程频繁报错。看到 C++ 报错,很多人第一反应是代码写错了,结果最后发现只是 NDK 版本不对。解决方式是在模块 build.gradle 里明确指定ndkVersion,并在 SDK Manager 或命令行中安装对应的 NDK 版本,让本机和项目声明完全对齐。
如果项目根本不需要原生代码,直接关掉 externalNativeBuild,或者把相关依赖模块移除,能省下一整类问题。这个案例也再次说明,下载编译失败的原因千奇百怪,但大多数都能在“版本对齐”和“环境配置”两个方向找到答案。把这些案例记下来,比临时翻文档有用得多。
5. 要把这份笔记做成“持续更新”,我的整理思路
5.1 建立自己的排错记录模板
我建议每个 Android 开发者维护一份本地排错记录,哪怕不公开发表也很有价值。模板很简单:日期、项目类型、Gradle/AGP/JDK 版本、完整报错关键行、根因、修复动作、备注。遇到新的失败时,先搜索旧记录里有没有相同报错。很多人二次踩到同一个坑,就是因为全靠记忆,没有留下可检索的记录。
记录时不要只抄结论,要把排查链路写下来。比如从报错关键字看到Could not resolve,然后怎么判断是仓库问题而不是版本问题,中间做了什么测试。这些思考路径才是最有价值的,下次遇到相似问题可以直接复用,而不需要从头开始分析。
5.2 升级依赖前先做三件事
第一,查兼容性矩阵。AGP、Gradle、JDK 的版本关系在官方文档里有明确说明,每次升级前花十分钟查清楚,比之后折腾一整晚报错要划算。第二,在干净分支上升级。不要在功能开发到一半的分支上直接动构建工具链,否则报错和代码问题混在一起,极难定位。第三,逐模块编译。改完插件版本先跑./gradlew :app:compileDebugKotlin,通过之后再跑全量构建。忌讳一次性把 AGP、Kotlin、Gradle 全部升到最新,多个新版本叠加后报错会很复杂。
如果项目里存在依赖覆盖,还要留意./gradlew dependencies的输出,看有没有某个依赖被偷偷升级到不兼容的版本。这个命令在排查“sync 成功但运行报错”时非常有用,比怀疑业务代码靠谱得多。
5.3 关于环境重构的一点个人体会
遇到莫名其妙的失败,很多人第一反应是重装 IDE、清空所有缓存甚至重装系统,但绝大部分情况下不需要。我推荐的重建顺序是:先停 daemon,清 Gradle 缓存,换镜像仓库,再核对 SDK/JDK/版本矩阵;还不行才考虑重建项目。重装只是把问题搬到另一个环境里,根因还在那里。
持续更新这份笔记,本质上是在给自己建立一套人肉缓存。每次遇到新的失败类型,我都会按前面的模板补一条,然后更新对应的章节。等你把排查模板也用起来,多半也能沉淀出自己的版本。希望这套方法能让正在被下载编译失败折磨的你,少交点无谓的学费。