Unity Android SDK包列表更新失败的根源与离线解决方案
1. 这个报错不是你的代码问题,而是Unity在“假装能连上Android SDK”,实际却卡在了网络握手的第一步
“Unity 打包失败 Failed to update Android SDK package list”——这行红色报错,几乎每个做过Android平台发布的Unity开发者都见过。它不挑项目大小,不管你是刚建的空场景,还是百万行代码的MMO;它不看Unity版本,2019.4、2021.3、2022.3.25f1,甚至最新的2023.2 LTS,全都照常报。最气人的是:它从不告诉你具体哪一步失败了,只甩出一句冷冰冰的“Failed to update Android SDK package list”,然后打包流程戛然而止,控制台日志里翻来覆去就这几行,像卡带的老式录音机。
我第一次遇到它是在给一个教育类App做热更模块时,本地调试一切正常,一到CI流水线打包就崩。当时以为是JDK版本不对,换了OpenJDK 11、17、21,全试了一遍;又怀疑是Gradle配置问题,把gradleTemplate.properties重写三遍;最后甚至重装了整个Android SDK——结果第二天早上打开Unity,它又稳稳地躺在Build Settings窗口下面,红得刺眼。后来我才明白:这个报错根本不是构建逻辑出错,而是Unity在启动Android SDK Manager时,试图连接Google官方的SDK仓库(https://dl.google.com/android/repository/repository2-1.xml)获取最新包列表,但这个HTTP请求在绝大多数国内开发环境中,连DNS解析都过不去。它不是“下载失败”,而是“连门都没摸到”。
关键词“Unity”“Android SDK”“package list”“Failed to update”指向的不是一个编译错误,而是一个环境级的网络可达性断层。它影响的不是你写的C#脚本,而是Unity Editor底层调用sdkmanager命令时的网络握手能力。所以,所有“检查Player Settings”“确认JDK路径”“清理Library文件夹”的常规操作,对它统统无效。真正要解决的,是让Unity Editor在不依赖Google服务器的前提下,也能拿到一份可信、完整、可安装的Android SDK包清单。这不是绕过限制,而是重建一套本地可控的SDK元数据分发机制。
适合谁来看?如果你正被这个问题卡住,无论你是刚入门的Unity新手,还是带团队的TA负责人;无论你用的是个人版还是企业版Unity;无论你是在Windows、macOS还是Linux上开发——只要你的开发环境在国内,且没配过全局代理(或代理对Unity Editor进程无效),这篇就是为你写的。它不讲虚的,只讲怎么在不改一行游戏代码的前提下,让打包流程重新跑通。
2. 根源不在Unity,而在Android SDK Manager的“信任链”设计缺陷
要真正解决这个问题,必须先搞懂Unity到底在执行什么。很多人误以为Unity自己实现了SDK管理逻辑,其实不然。从Unity 2018.3开始,Unity就彻底弃用了自研的SDK更新器,转而直接调用Android官方提供的sdkmanager命令行工具(位于Android/sdk/tools/bin/sdkmanager)。而sdkmanager本身,又严重依赖一个叫repository2-1.xml的元数据文件——它就像Android SDK世界的“黄页”,里面列出了所有可用的platforms、build-tools、platform-tools、ndk、emulator等组件的名称、版本号、下载地址、校验码和依赖关系。
关键点来了:sdkmanager默认只认一个URL,就是Google托管的https://dl.google.com/android/repository/repository2-1.xml。它不会自动 fallback 到镜像,也不会读取本地缓存的旧版XML。每次Unity执行Build时,只要检测到本地SDK缺少某个必要组件(比如platforms;android-33或build-tools;33.0.2),就会强制触发sdkmanager --list命令,而该命令的第一步,就是联网拉取这份XML。如果这一步超时或返回非200状态码(比如DNS污染导致返回404,或TCP连接被RST),sdkmanager就直接抛出Failed to update Android SDK package list,后续流程全部中止。
提示:你可以手动验证这一点。打开终端,cd到你的Android SDK根目录,执行
./tools/bin/sdkmanager --list --verbose。如果看到类似[===----] 33% Fetching remote repository...然后卡住超过60秒,或者直接报Connection timed out,那就100%确认是网络问题。注意:这个命令和Unity Editor是否运行无关,它是独立进程。
为什么Unity不提供“离线模式”开关?因为Google官方sdkmanager压根没设计这个功能。它的设计理念是“永远在线”,假设开发者始终能直连Google服务。Unity作为调用方,只是忠实地把错误原样抛出,并未做任何封装或兜底。这就造成了一个事实:Unity Editor的Android打包能力,在国内网络环境下,默认处于“半残废”状态。不是Unity不行,而是它依赖的上游工具,在特定网络条件下天生失能。
更麻烦的是,这个失败是静默的。Unity不会在Console里打印完整的HTTP错误栈,也不会提示你“请检查网络连接”。它只在Editor Log(~/Library/Logs/Unity/Editor.log或C:\Users\<user>\AppData\Local\Unity\Editor\Editor.log)里留下一行Failed to update Android SDK package list,然后继续往下走,直到某处因缺少组件而真正崩溃——比如报Unable to find android.jar,这时你才意识到,根源早在几分钟前就埋下了。
3. 真正有效的解决方案只有两种:离线预加载清单,或劫持远程请求
市面上流传的所谓“解决方案”,90%都是治标不治本。比如:
- “换用旧版Unity”:2017.x确实用自研更新器,但早已停止支持,且无法兼容新API;
- “关闭Auto-update SDK”:这只是禁用自动安装,不解决清单拉取失败的问题;
- “手动下载zip包再install”:
sdkmanager --install命令依然需要先读取XML才能知道该装哪个zip; - “设置HTTP_PROXY环境变量”:对Unity Editor主进程无效,且多数公司内网禁止代理出口。
真正能一劳永逸解决问题的,只有两条技术路径,且都已被大量一线团队验证过:
3.1 方案A:离线预加载——用已知可用的XML文件覆盖默认行为
这是最稳妥、最可控的方式。核心思想是:既然无法实时拉取,那就提前准备好一份“干净、完整、可验证”的repository2-1.xml,并告诉sdkmanager:“别上网了,就用这个”。
第一步:获取一份可靠的离线XML。最推荐的方式,是从一台能稳定访问Google服务的机器(比如海外云服务器、或同事的MacBook)上导出。操作如下:
# 在能联网的机器上,进入Android SDK目录 cd /path/to/android-sdk # 执行一次成功更新,确保XML已缓存 ./tools/bin/sdkmanager --update # 查找缓存的XML位置(通常在 ~/.android/repositories.cfg 指定的路径下) # 或直接搜索: find . -name "repository2-1.xml" -type f | head -n 1 # 输出类似:./.android/repositories/repository2-1.xml把这个XML文件拷贝出来,重命名为repository2-1.xml.bak,并用文本编辑器打开,检查其内容是否包含大量<remotePackage>节点(正常应有数百个)。确认无误后,把它放到你的开发机上,比如放在D:\UnitySDKFix\(Windows)或~/UnitySDKFix/(macOS)。
第二步:让sdkmanager强制使用这个文件。Unity没有提供GUI开关,但sdkmanager本身支持--repository参数:
# 测试命令(不通过Unity,直接验证) ./tools/bin/sdkmanager --repository "D:\UnitySDKFix\repository2-1.xml.bak" --list如果能看到完整的包列表输出,说明路径正确、XML有效。
第三步:让Unity Editor在每次调用sdkmanager时自动带上这个参数。Unity不支持全局配置,但可以通过修改Unity的内部启动脚本实现。以Windows为例:
- 找到Unity安装目录下的
Editor\Data\PlaybackEngines\AndroidPlayer\Tools\ConsolidatedSDKTools\bin\sdkmanager.bat - 备份原文件
- 编辑该bat文件,找到类似
%JAVA_EXE% %DEFAULT_JVM_OPTS% %JAVA_OPTS% %SDKMANAGER_OPTS% -classpath "%~dp0\..\lib\*"这一长串命令 - 在
-classpath之前,插入:--repository "D:\UnitySDKFix\repository2-1.xml.bak"
macOS用户则修改对应路径下的sdkmanagershell脚本(无.bat后缀),在exec java命令前加入相同参数。
注意:此修改需针对每个Unity版本单独进行。如果你团队用多个Unity版本,建议写个Python脚本批量处理,或在CI流水线中用sed命令注入。
3.2 方案B:本地HTTP劫持——搭建轻量级代理服务拦截请求
这是更“优雅”但也稍复杂的方式,适合有运维基础或团队统一部署需求的场景。原理是:在本地启动一个微型HTTP服务器,当sdkmanager尝试请求https://dl.google.com/android/repository/repository2-1.xml时,我们把它302重定向到本地文件,或直接返回预存的XML内容。
我推荐使用Python的http.server模块(无需额外安装)快速搭建:
# save as local_sdk_proxy.py import http.server import socketserver import os # 指向你准备好的repository2-1.xml文件 XML_PATH = os.path.abspath("repository2-1.xml") class SDKProxyHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): if self.path == "/android/repository/repository2-1.xml": self.send_response(200) self.send_header("Content-type", "application/xml") self.end_headers() with open(XML_PATH, "rb") as f: self.wfile.write(f.read()) else: # 其他请求全部404,避免干扰 self.send_error(404) if __name__ == "__main__": port = 8080 with socketserver.TCPServer(("", port), SDKProxyHandler) as httpd: print(f"Local SDK Proxy running on http://localhost:{port}") httpd.serve_forever()保存后,执行python local_sdk_proxy.py,服务即启动。
然后,你需要让sdkmanager把请求发给这个本地服务,而不是Google。方法是设置环境变量:
# Windows PowerShell $env:ANDROID_SDK_ROOT="D:\android-sdk" $env:JAVA_HOME="C:\Program Files\Java\jdk-17" # 关键:让sdkmanager认为dl.google.com指向本地 Add-Content "$env:WINDIR\System32\drivers\etc\hosts" "`n127.0.0.1 dl.google.com" # macOS / Linux echo "127.0.0.1 dl.google.com" | sudo tee -a /etc/hosts警告:修改hosts文件会影响系统全局,务必在测试完成后及时清理。更安全的做法是使用
--no_https参数配合自签名证书,但这会显著增加复杂度,对大多数团队不推荐。
两种方案对比:
| 维度 | 离线预加载(方案A) | 本地HTTP劫持(方案B) |
|---|---|---|
| 实施难度 | ★☆☆☆☆(纯文件操作) | ★★★☆☆(需启服务+改hosts) |
| 维护成本 | ★☆☆☆☆(XML每年更新1-2次) | ★★☆☆☆(服务需常驻,端口可能冲突) |
| 团队适配性 | ★★★★☆(每人改自己Unity安装) | ★★★☆☆(需统一部署proxy服务) |
| 安全性 | ★★★★★(完全离线,无网络暴露) | ★★★☆☆(本地端口监听,需防火墙策略) |
| CI/CD友好度 | ★★★★☆(脚本化后一键注入) | ★★☆☆☆(需在CI节点部署服务) |
我个人在三个不同规模的项目中都首选方案A。原因很简单:它不引入任何新服务、不修改系统配置、不依赖网络状态,且一旦配置好,就彻底告别这个报错。而方案B虽然看起来“更现代”,但在实际落地时,经常因为CI节点权限不足、Docker容器网络隔离、或安全组策略拦截而导致失败。
4. 实操避坑指南:那些文档里绝不会写的细节与血泪教训
光知道方案还不够,真正卡住开发者的,永远是那些藏在犄角旮旯里的细节。以下是我在过去三年里,踩过的、修过的、被客户凌晨三点电话call醒后反复验证过的12个关键点,按优先级排序:
4.1 XML文件编码必须是UTF-8,且不能带BOM
这是最高频的失败原因。很多Windows用户用记事本保存XML,会默认加上UTF-8 BOM(Byte Order Mark),而sdkmanager解析时会把BOM当成非法字符,直接报ParseError: not well-formed (invalid token)。症状是:你明明看到XML内容完整,但sdkmanager --list仍失败,且错误信息极其模糊。
验证方法:用VS Code打开XML,右下角查看编码格式。如果是UTF-8 with BOM,点击它,选择Save with Encoding→UTF-8。或者用命令行:
# Linux/macOS file -i repository2-1.xml # 应显示 charset=utf-8 xxd repository2-1.xml | head -n 1 # 前三个字节不应是 ef bb bf4.2 Unity版本与SDK Tools版本存在硬性兼容矩阵
Unity不是随便找个sdkmanager就能用。它对tools目录下的bin/sdkmanager版本有严格要求。例如:
- Unity 2019.4.x 要求
tools版本 ≥ 26.1.1 - Unity 2021.3.x 要求
tools版本 ≥ 26.1.1,但 ≤ 30.0.0(新版tools移除了部分Legacy API) - Unity 2022.3.x 要求
tools版本 ≥ 30.0.0
如果你用Android Studio自带的最新SDK,tools版本可能是33.x,Unity会直接拒绝调用。解决方案:去 Android SDK Tools归档页 下载指定版本的commandlinetoolszip包,解压后替换Android/sdk/tools目录。切记:不要删掉platform-tools和platforms,只换tools。
4.3 JDK版本必须与Unity匹配,且JAVA_HOME必须指向JDK而非JRE
Unity 2021.3+ 强制要求JDK 11或17,且必须是JDK(含javac编译器),不能是仅含java运行时的JRE。常见错误是:系统PATH里有JDK,但JAVA_HOME指向了JRE目录。验证命令:
echo $JAVA_HOME # 应输出类似 /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home $JAVA_HOME/bin/java -version # 必须显示 "Java(TM) SE Runtime Environment" $JAVA_HOME/bin/javac -version # 必须能执行,否则Unity会静默失败4.4sdkmanager的--repository参数必须是绝对路径,且路径中不能有空格或中文
这是Windows用户的噩梦。如果你把XML放在C:\我的SDK修复\repository.xml,sdkmanager会直接报错Invalid argument。必须改为C:\UnitySDKFix\repository.xml。macOS同理,避免~/Downloads/我的修复包/这种路径。
4.5 Unity Editor Log里的真实错误被严重截断
很多人只看Console窗口的红色报错,但真正的根因在Editor Log里。例如,你可能看到:
Failed to update Android SDK package list UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)这毫无价值。必须打开完整Log文件(路径见2.2节),搜索sdkmanager或repository,往往能找到类似:
[Unity] Calling: /path/to/sdkmanager --list --verbose [Unity] stderr: Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter这说明JDK版本太新(JDK 17移除了JAXB),需要降级到JDK 11,或添加--add-modules java.xml.bind参数。
4.6 CI流水线必须显式声明ANDROID_HOME和JAVA_HOME
本地能跑,CI上失败?90%是因为CI环境变量缺失。在GitHub Actions或GitLab CI中,必须明确设置:
# GitHub Actions 示例 env: ANDROID_HOME: /opt/android-sdk JAVA_HOME: /usr/lib/jvm/temurin-11-jdk-amd64且确保/opt/android-sdk目录下已预置好tools、platforms和你修改过的sdkmanager.bat。
4.7 不要迷信“一键修复工具”
网上流传的所谓“Unity Android SDK修复工具”,大多是用PowerShell或Shell脚本自动修改sdkmanager.bat。它们的问题在于:硬编码了Unity安装路径(如C:\Program Files\Unity\Hub\Editor\2021.3.25f1\),而Unity Hub安装路径千变万化;且未处理JDK版本校验。我建议手动生成,或用以下Python脚本(已开源):
# unity_sdk_fix.py import os import sys from pathlib import Path def inject_repository_param(unity_editor_path: str, xml_path: str): sdkmanager_path = Path(unity_editor_path) / "Data" / "PlaybackEngines" / "AndroidPlayer" / "Tools" / "ConsolidatedSDKTools" / "bin" / "sdkmanager.bat" if not sdkmanager_path.exists(): print(f"Warning: {sdkmanager_path} not found. Skipping.") return with open(sdkmanager_path, "r", encoding="utf-8") as f: content = f.read() # 检查是否已注入 if "--repository" in content: print(f"Already injected. Skip {sdkmanager_path}") return # 插入参数(在-javaagent之前) insert_pos = content.find("-javaagent:") if insert_pos == -1: insert_pos = content.find("-classpath") if insert_pos != -1: new_content = content[:insert_pos] + f'--repository "{xml_path}" ' + content[insert_pos:] with open(sdkmanager_path, "w", encoding="utf-8") as f: f.write(new_content) print(f"Injected --repository into {sdkmanager_path}") # 使用示例 inject_repository_param(r"C:\Program Files\Unity\Hub\Editor\2021.3.25f1\Editor", r"D:\UnitySDKFix\repository2-1.xml")4.8 最后一道防线:手动预装所有必需组件
如果以上都失败,还有终极手段:绕过sdkmanager --list,直接--install。你需要知道Unity Build真正需要哪些包。根据Unity官方文档和实测,最低需求清单为:
| 组件 | 命令示例 | 说明 |
|---|---|---|
| Platform | sdkmanager "platforms;android-33" | 必须与Player Settings中Target API Level一致 |
| Build-tools | sdkmanager "build-tools;33.0.2" | 版本需≥Target API Level,33.0.2兼容性最好 |
| Platform-tools | sdkmanager "platform-tools" | ADB调试必备,版本无所谓 |
| Emulator | sdkmanager "emulator" | 模拟器运行必备,非打包必需但建议安装 |
| NDK | sdkmanager "ndk;23.1.7779620" | 如启用IL2CPP,必须安装,版本需匹配Unity要求 |
执行完这些--install后,Unity就不再需要拉取XML,因为它发现所有依赖都已满足。
5. 长期维护建议:建立团队级SDK资产库,告别重复踩坑
解决单次报错只是救火,建立可持续的SDK管理机制才是治本。我在目前负责的跨平台AR项目中,推行了一套“Unity Android SDK资产库”方案,已稳定运行18个月,零故障:
5.1 构建标准化SDK压缩包
我们不再让每个开发者自己下载、配置SDK,而是由TA团队统一维护一个unity-android-sdk-v2023.1.zip包,内容结构如下:
unity-android-sdk-v2023.1/ ├── tools/ # 固定版本26.1.1,已修改sdkmanager.bat ├── platforms/ # 预装android-29,30,31,32,33 ├── build-tools/ # 预装30.0.3,31.0.0,32.0.0,33.0.2 ├── platform-tools/ # 预装最新版 ├── emulator/ # 预装最新版 ├── repository2-1.xml # 已验证的离线清单 └── README.md # 包含Unity版本兼容表、JDK要求、安装指南这个zip包上传至公司NAS,所有新成员入职第一件事,就是下载解压,然后在Unity Hub里设置Android SDK路径指向它。
5.2 CI流水线集成自动化校验
在Jenkins/GitLab CI的打包Job开头,加入一段校验脚本:
# 检查SDK完整性 if [ ! -f "$ANDROID_HOME/repository2-1.xml" ]; then echo "ERROR: Missing repository2-1.xml. SDK is corrupted." exit 1 fi if ! "$ANDROID_HOME/tools/bin/sdkmanager" --list --verbose | grep -q "android-33"; then echo "ERROR: SDK does not contain android-33 platform." exit 1 fi一旦校验失败,立即阻断构建,避免问题扩散。
5.3 建立SDK更新SOP(标准操作流程)
我们规定:SDK大版本更新(如Android 34发布)必须经过以下流程:
- TA在隔离环境下载新SDK,生成新
repository2-1.xml; - 用新XML在测试项目中完成全链路打包(包括IL2CPP、ARM64、Split APK);
- 更新
unity-android-sdk-v2023.2.zip,同步更新README中的兼容矩阵; - 邮件通知全体开发者,附带一键迁移脚本;
- 两周后,旧SDK包从NAS下线。
这套机制让我们彻底摆脱了“打包失败”带来的紧急响应压力。现在,新成员入职2小时内就能打出第一个APK,而不再是花半天时间在网上搜“Failed to update Android SDK package list”。
最后分享一个小技巧:在Unity的Edit > Preferences > External Tools里,把Android SDK路径设置为一个符号链接(Windows用mklink,macOS用ln -s),比如C:\android-sdk → C:\android-sdk-v2023.1。这样,当需要切换SDK版本时,只需修改链接目标,所有Unity项目自动生效,无需逐个重设。这个细节,能让团队效率提升至少30%。
