☰
解决LiveKitWebRTC缺少dSYM导致Crashlytics上传失败问题
2026/10/3 11:14:43 网站建设 项目流程

1. 这行报错到底在说什么

先把我当时见到这行报错的原话搬出来:error: Upload Symbols Failed: The archive did not include a dSYM for the LiveKitWebRTC.framework with the UU...

看到这个报错的第一反应,大多数人和我一样:我的代码又没改,LiveKit 升级之后怎么突然就炸了?查了一圈之后才明白,这行报错既不是编译错误,也不是代码逻辑问题,而是 Firebase Crashlytics 在归档阶段没找到 LiveKitWebRTC.framework 对应的 dSYM 调试符号文件。

先解释一个细节:报错末尾的UU不是什么隐藏 flag,它其实是UUID这个单词被截断后的残影,完整的报错通常是with the UUID 4C7A2B3E-xxxx-xxxx-xxxx-xxxxxxxxxxxx这种格式。顺带说一句,U本身不是十六进制字符,所以看到UU基本可以断定是日志被裁剪了,不要被它带偏。

这个报错会出现在哪两个地方?一是你本地 Xcode 里执行 Product > Archive 时的构建日志,二是 CI 上跑xcodebuild archive时的终端输出。触发它的是 Crashlytics 的 Run Script 阶段,也就是集成 Firebase Crashlytics 时自动加进工程里的那个脚本,里面通常调用了${PODS_ROOT}/FirebaseCrashlytics/upload-symbols或FirebaseCrashlytics/run。脚本干的事情很简单:把这次归档生成的 dSYM 文件上传到 Crashlytics 服务器,供后续崩溃解析使用。

至于谁会撞上这个问题,画像非常清晰:iOS 端接了 LiveKit SDK 做音视频通话、同时用 Firebase Crashlytics 做崩溃上报的工程。LiveKitWebRTC.framework 是 LiveKit 依赖的 WebRTC 预编译库,它本身没有参与你工程的源码编译,所以 Xcode 默认不会为它生成 dSYM。而 Crashlytics 的上传脚本在归档包里翻了一圈,发现 LiveKit 这个框架对应的符号文件不存在,于是老老实实报了个错给你。

这行报错带来的直接后果有两个:第一,崩溃发生在 WebRTC 内部的帧,解析出来全是十六进制内存地址,看不到函数名、看不到调用栈,排查音频采集、网络丢包这类 WebRTC 层问题时基本靠猜;第二,脚本返回非零状态,在某些 CI 配置下会直接把整个归档流程标红,严重的会阻断发布流水线。所以它不是“看着吓人、其实没事”的警告,而是需要正经处理的集成问题。

2. dSYM 到底是个啥:先弄清楚原理再动手

2.1 dSYM 是苹果给的“崩溃翻译字典”

dSYM 这个概念,很多 iOS 开发者用了好几年 Crashlytics 也没真正理解过。简单说,dSYM 是 Debug Symbols 的缩写,文件格式是 DWARF,它在编译链接阶段由dsymutil工具把调试信息从 Mach-O 二进制里提取出来,单独打包成一个.dSYM目录。

为什么要单独拆出来?因为发布到 App Store 和用户手机上的 App 是需要裁剪体积的,里面的符号表、函数名、变量名这些调试信息对运行没有意义,留着只会增大包体。但崩溃上报工具拿到的是一堆内存地址,比如0x1023f4a1c,它需要把这些地址翻译回-[LiveKitLocalParticipant didAddTrack:]这样的源码位置,翻译的依据就是 dSYM。

你可以把 dSYM 理解成一本“反编译字典”:崩溃报告是乱码电报,dSYM 是密码本,两者对上了才能还原出可读的调用栈。没有密码本,电报就是一堆毫无意义的数字。

2.2 UUID 是 dSYM 与二进制的绑定关系

这里的关键是“对上”这两个字。每一份 Mach-O 二进制文件在构建时都会生成一个唯一的 UUID,这个 UUID 会同时写入二进制本身和对应的 dSYM 文件里,相当于同一个“人”的两张身份证。

Crashlytics 的上传脚本工作流程是这样的:遍历归档包里的 App 以及所有嵌入的 framework,逐个读出每个二进制的 UUID,再拿着这批 UUID 去扫描归档包里的 dSYMs 目录,看看能不能找到匹配的 dSYM。匹配成功就上传,匹配不上就打印一条类似本文标题的报错,明确告诉你:LiveKitWebRTC.framework的 UUID 是xxxx,但我在归档里没找到它的 dSYM。

所以你在报错里看到的那个 UUID 就是 LiveKitWebRTC 二进制的“身份证号码”。如果你手里有同样的 UUID 的 dSYM,上传之后崩溃解析立刻就能用;如果拿不到,WebRTC 内部的栈帧就永远是裸地址。

2.3 Xcode 默认只对你“亲手编译”的代码生成 dSYM

理解了上面两条,最后一块拼图就是:Xcode 的 dSYM 是从“编译”这个动作里生成的。你自己的源码、你 pod 里以源码形式集成的第三方库,在DEBUG_INFORMATION_FORMAT设置为DWARF with dSYM File时,链接完会自动产出 dSYM 并放进 xcarchive 的 dSYMs 目录,这部分一般不会出问题。

但预编译二进制是另一回事。LiveKitWebRTC.framework 是 LiveKit 团队提前用 WebRTC 源码编好、打包成 xcframework 分发给你的。Xcode 只是把它“嵌入”进 App,并不会重新编译它,自然也就不会为它生成 dSYM。这个逻辑用生活化的话讲就是:你自己炒的菜,锅铲火候都是你定的,食谱你当然有;但你在超市买的一包预制菜,调料包配方和生产工艺都在厂家手里,你想复盘这道菜怎么做的,得找厂家要食谱,而不是指望自家厨房凭空变出来。

3. 为什么偏偏是 LiveKitWebRTC.framework 的 dSYM 丢了

3.1 LiveKitWebRTC 是一个预编译的二进制库

LiveKit 做的是实时音视频基础设施,iOS 端 SDK 叫 LiveKitClient,底层依赖一个定制过的 WebRTC 版本,就是 LiveKitWebRTC。这个库的体量很大,从源码编译一遍要数小时,所以官方通常直接发布预编译好的 xcframework 二进制。

问题就出在这个“预编译”上。预编译框架的 dSYM 只有厂商自己在构建时生成,然后随包分发。LiveKit 团队在大部分版本里确实会把 dSYM 一起打包在发布产物里,但这里有个坑:dSYM 是否被正确送到你的归档包里,中间还隔着 Xcode、CocoaPods、SPM 的层层传递,任何一环漏了,最终 Crashlytics 就报缺文件。

我在实际项目里遇到过三种情况:一是官方某些版本在 Release 资源里没有附带 dSYM;二是官方带了,但 CocoaPods 的vendored_frameworks只把 .framework 拷进了 App,dSYM 留在 Pods 目录里没被拾取;三是 Xcode 归档时,xcframework 内部的 dSYM 并不会像 App 自己的 dSYM 那样自动被收集进xcarchive/dSYMs/目录。

3.2 归档时 Xcode 并不负责“收集”第三方 dSYM

很多人误以为,只要框架的 xcframework 里带着 dSYM,归档时 Xcode 就会自动把它放进 dSYMs 目录。实测下来并不是。Xcode 的归档流程对“自己编译出来的 dSYM”有明确的收集路径,但对“预编译框架自带的 dSYM”处理得很随意:有的版本会复制过去,有的版本不会,而且这个行为跟 Xcode 版本、框架的目录结构都有关系。

更麻烦的是,Crashlytics 的upload-symbols脚本按照惯例只扫${DWARF_DSYM_FOLDER_PATH},也就是 xcarchive 的 dSYMs 目录。就算声明在 LiveKitWebRTC.xcframework 内部某个架构切片目录里确实躺着LiveKitWebRTC.framework.dSYM,只要它没被复制到 archive 的 dSYMs 目录,脚本就认为它“不存在”。这是一个典型的“东西在,但没送到对的地方”的坑。

3.3 CocoaPods 和 SPM 接入方式会让问题更隐蔽

接入方式不同,症状也不太一样。用 CocoaPods 时,LiveKitWebRTC 会被安装在Pods/LiveKitWebRTC/目录下,很多时候 dSYM 就在这个目录里静静躺着,只是没进 archive。用 SPM 时更隐形,SPM 的二进制 target 下载后会被解压缓存到DerivedData/.../SourcePackages/artifacts/下,这个路径藏在层层目录里,平时根本不会有人去翻,而且清理 DerivedData 之后缓存会被重新下载,路径又会变。

这也是为什么很多人在网上搜这行报错,搜到的答案五花八门:有人说删 DerivedData、有人说关掉 Crashlytics 脚本、有人干脆建议换掉 LiveKit。其实都是没有定位到根因。根因就是一句话:Crashlytics 需要一个特定 UUID 的 dSYM,而你的归档包里恰好没有这个文件。接下来要做的不是绕过,而是把这个文件找出来,或者让它自动出现在该出现的位置。

4. 实操修复:从定位到解决的四条路线

4.1 动手前先做三件事:查日志、看归档、对 UUID

不管选哪种修复方案,第一步建议先把现场摸清楚,别上来就改工程配置。我用的排查三步走,按顺序执行:

# 1. 看看你的 xcarchive 里到底有哪些 dSYM ls -la "/path/to/YourApp_2024-xx-xx.xcarchive/dSYMs/"

这一步能直接确认:归档包里只有 App 自己的 dSYM,还是已经带了部分第三方库的 dSYM,唯独少了 LiveKitWebRTC。

# 2. 拿到 LiveKitWebRTC 二进制的 UUID dwarfdump --uuid "/path/to/YourApp.app/Frameworks/LiveKitWebRTC.framework/LiveKitWebRTC"

把 App 包里的框架路径换成你实际路径,输出会类似:

UUID: 4C7A2B3E-1234-5678-9ABC-DEF012345678 (arm64) /path/to/LiveKitWebRTC

记下这个 UUID,接下来所有操作都以它为基准。

# 3. 整个硬盘搜一遍,看看 LiveKitWebRTC 的 dSYM 到底存不存在 find ~/Library/Developer/Xcode/DerivedData -type d -name "LiveKitWebRTC.framework.dSYM" 2>/dev/null

这一步会有几种结果:完全搜不到,说明官方包没带或者没被下载下来;搜到了,那就对比dwarfdump --uuid的结果,UUID 对得上就能用,对不上说明版本不匹配。做完这三步,你基本就知道该走下面的哪个方案了。

4.2 方案 A:从 LiveKit 官方获取 dSYM 并手动上传

这是最直接、也是我推荐优先尝试的方案。先确认你工程里锁定的 LiveKitWebRTC 版本,用 CocoaPods 就看Podfile.lock,用 SPM 就看Package.resolved:

- LiveKitWebRTC (2.0.x)

确认版本后,去 LiveKit 的 GitHub Release 页面找对应版本的预编译产物。以livekit/webrtc-ios或livekit/client-sdk-ios的 Releases 为例,通常每个版本除了 xcframework 压缩包之外,还会有一个带 dsym 字样的产物,下载解压后就能拿到LiveKitWebRTC.framework.dSYM。

拿到 dSYM 之后,手动执行上传命令。如果你的工程用 CocoaPods,upload-symbols脚本在这里:

"${PODS_ROOT}/FirebaseCrashlytics/upload-symbols" \ -gsp "${PROJECT_DIR}/GoogleService-Info.plist" \ -p ios \ "/path/to/downloaded/LiveKitWebRTC.framework.dSYM"

执行完看到类似Uploading symbols for 4C7A2B3E-... ... Successfully uploaded的输出就算成了。然后去 Firebase 控制台,进入 Crashlytics 的 dSYMs 页面,应该能看到这个 UUID 躺在列表里,状态是 Uploaded。

这里提醒一句:下载的 dSYM 版本必须和 App 里嵌入的 LiveKitWebRTC 完全一致。因为 WebRTC 每次构建生成的 UUID 都不相同,版本差一个小版本,UUID 都对不上,上传了也白传。

4.3 方案 B:在 Xcode 里加一段脚本自动补齐 dSYM

手动上传能解决一次,但解决不了“每次归档都报错”的问题。如果你的 LiveKitWebRTC 包本身带着 dSYM,只是没有被复制进归档包,那最佳方案是在 Xcode 的 Build Phases 里加一段“补位”脚本,让它在 Crashlytics 上传脚本之前把 dSYM 复制到DWARF_DSYM_FOLDER_PATH指向的目录。

具体操作:选中 Target > Build Phases > 点加号 > New Run Script Phase,把这段脚本拖到 Crashlytics 的 Run Script 之前,然后粘贴以下内容:

# 自动把 Pods 里的 LiveKitWebRTC dSYM 复制进 xcarchive 的 dSYMs 目录 if [ -d "${DWARF_DSYM_FOLDER_PATH}" ]; then DSYM_SOURCE=$(find "${PODS_ROOT}/LiveKitWebRTC" -path "*LiveKitWebRTC.framework.dSYM" -type d 2>/dev/null | head -1) if [ -n "${DSYM_SOURCE}" ]; then echo "Copying LiveKitWebRTC dSYM to ${DWARF_DSYM_FOLDER_PATH}" cp -Rf "${DSYM_SOURCE}" "${DWARF_DSYM_FOLDER_PATH}/" else echo "Warning: LiveKitWebRTC dSYM not found in Pods" fi fi

这段脚本的逻辑是:在${PODS_ROOT}/LiveKitWebRTC目录下递归查找LiveKitWebRTC.framework.dSYM目录,找到就复制到归档的 dSYMs 目录。因为find可能命中多个路径,我用head -1取第一个,实际操作中如果命中多个,建议用-path "*Release-iphoneos*"这样的条件进一步缩小范围。

如果你是 SPM 接入,路径不在PODS_ROOT下,可以把查找范围改成~/Library/Developer/Xcode/DerivedData:

DSYM_SOURCE=$(find ~/Library/Developer/Xcode/DerivedData \ -path "*SourcePackages/artifacts/*" \ -name "LiveKitWebRTC.framework.dSYM" -type d 2>/dev/null | head -1)

改完脚本重新 Archive 一次,Crashlytics 的报错应该就消失了。这个方案我实测下来最省心,一次配置,团队里其他人拉代码也能直接生效。

4.4 方案 C:从源码自己编译一份带符号的 LiveKitWebRTC

如果官方包压根不提供 dSYM,或者你的版本比较老找不到对应产物,那就只剩两条路:要么接受 WebRTC 层不能符号化,要么自己编译。自己编译这个选项听着吓人,实际效果是最好的,因为源码构建必然产出完整 dSYM,而且 UUID 一定匹配。

LiveKit 官方仓库livekit/webrtc里有构建脚本。大致流程是:先装 depot_tools,这是 Chromium 系的构建工具链,然后同步 WebRTC 源码,最后执行 iOS 构建脚本:

git clone https://github.com/livekit/webrtc.git cd webrtc # 按 README 配置 depot_tools 环境变量 # 同步依赖,这一步会下载大量代码,耗时较长 python tools_webrtc/ios/build_ios_libs.py --debug

编译产物里会同时得到.framework和.dSYM。拿到之后,要么用这份自编译的框架替换掉 Pods 里的 LiveKitWebRTC,要么只把 dSYM 提取出来走方案 A 手动上传。

说句实在话,这个方案我在用一个内部版本时才不得不走。如果你只是想让 Crashlytics 不报错、WebRTC 内部帧能看个大概,走方案 A 或 B 就够了;但如果你在做 WebRTC 定制开发,比如修改编码参数、调试抖动缓冲,那源码编译几乎是绕不开的。

4.5 方案 D:暂时绕过报错(不建议,但有适用场景)

网上不少人给出的“解法”是在 Crashlytics 脚本后面加|| true,让脚本无论成功失败都返回成功状态。这确实能让构建变绿,但代价是:这次归档的所有 dSYM 都不会被上传,不只是 LiveKitWebRTC 的,连你自己代码的崩溃解析也会挂掉,属于典型的因小失大。

我的建议是,即使真要绕过,也别一整行忽略。可以保留 Crashlytics 脚本的原始行为,只让它在“缺第三方 dSYM”这类情况下不阻断流程,比如写个包装脚本,解析输出里的错误关键字再决定返回码。更简单的做法是:先确认你代码自己的 dSYM 是否成功上传了,如果成功了,只是缺 WebRTC 这一份,那暂时不阻断归档是可以接受的。

适用场景也有:你的项目只是内部测试包,WebRTC 层崩溃栈很少去看,产品着急发版,官方 dSYM 又暂时拿不到。这时候“接受不完整符号化”比“卡住整条发布流水线”务实得多。但后续拿到 dSYM 后,记得手动补传一次,历史崩溃报告的解析会自动补全。

5. 常见问题排查与避坑清单

把我在处理这个报错过程中遇到的高频问题整理成了速查表,按“现象 -> 原因 -> 处理动作”对照着看比较省事:

现象常见原因处理动作
每次 Archive 都报相同的缺失 dSYM官方版本未附带 dSYM,或 dSYM 未进归档包方案 A 手动上传,或方案 B 加复制脚本
下载了 dSYM 但 UUID 对不上版本不一致,或同版本多次构建 UUID 不同严格按 Podfile.lock / Package.resolved 锁定版本
归档包里能看到 dSYM,但上传仍失败upload-symbols路径参数不对,或脚本扫描不到确认脚本里的${DWARF_DSYM_FOLDER_PATH}引用正确
SPM 集成时搜索不到 dSYMSPM 缓存被清理,xcframework 还没下载先执行一次构建,再在 DerivedData 中查找
只缺某一个架构切片的 dSYM官方 dSYM 只覆盖 arm64,缺失 armv7/x86_64确认崩溃设备架构,通常 arm64 覆盖绝大多数真机
升级 LiveKit 后又开始报错新版本 UUID 变了,旧 dSYM 失效重新下载对应新版本的 dSYM
报错不在 LiveKit 上,而在 GoogleWebRTC、Agora 等框架上同一类预编译框架问题,根因相同用同样思路找对应厂商的 dSYM

另外有几点实操里的注意事项,值得单独拎出来讲。第一,所有涉及路径的脚本,必须给变量加双引号,特别是DWARF_DSYM_FOLDER_PATH这种 Xcode 环境变量,路径里一旦有空格,不加引号就会断成两个参数,脚本直接挂掉。第二,upload-symbols脚本在某些老版本 Firebase 里依赖的外部工具链不同,如果你的 Firebase SDK 很老,建议先升级到较新版本,否则可能还会遇到 Java 运行时缺失之类的附加问题。第三,用find全盘查找时,如果命中多个 dSYM,务必用dwarfdump --uuid逐一比对,我曾经就因为head -1拿错了版本,白传了一次。

还有一个容易混淆的点:报错信息里如果出现的是GoogleService-Info.plist is missing而不是本文讨论的 dSYM 缺失,那是另一类问题,属于脚本找不到 Firebase 配置文件,和 dSYM 无关,别混在一起排查。

最后再说一个我自己踩过坑之后养成的习惯:现在每次发版前,我都习惯先在 Firebase 控制台的 Crashlytics > dSYMs 页面扫一眼,确认本次归档对应的 UUID 都已经上传。如果团队有 CI,还可以在归档流程里加一个自动校验步骤,列出 App 包内所有框架的 UUID,再和归档目录里的 dSYM 做一次 diff,缺哪个直接报出来。这样问题在归档阶段就暴露,而不是等到线上出崩溃、排查时才发现堆栈解析不出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询