Xcode 已经显示“正在附加”,云端 Mac 的 CPU 占用却接近空闲,断点从蓝色变成灰色,变量窗口也没有结果。此时连续点击停止、重启或删除 DerivedData,往往只会抹掉现场。更有效的做法是把问题拆成三层:目标进程是否可调试、LLDB 与目标之间的会话是否建立、当前产物能否找到匹配的符号。
先判断卡在哪一层
先记录发生时间、工程提交号、Scheme、Configuration、目标设备和启动方式,再观察 Xcode 状态。不要把“附加慢”“应用启动慢”和“符号解析慢”混为一类。
| 现象 | 优先检查 | 常见结论 |
|---|---|---|
| 应用未启动,LLDB 一直等待 | 目标进程、启动参数 | 进程退出或启动到了另一份产物 |
| 已显示进程 PID,界面无响应 | 进程状态、调试服务 | 目标暂停、阻塞或调试握手未完成 |
| 断点为空心或灰色 | 模块与符号 | 模块未加载、路径变化或 UUID 不匹配 |
| 能停住但看不到局部变量 | 优化级别、调试信息 | Release 优化导致变量被合并或消除 |
在另一个终端保存进程快照:
mkdir -p "$HOME/lldb-case"
date -u > "$HOME/lldb-case/time.txt"
ps -axo pid,ppid,state,%cpu,etime,command \
> "$HOME/lldb-case/processes.txt"
xcrun simctl list devices \
> "$HOME/lldb-case/simulators.txt"
state 中长期出现 U 可能意味着进程处于不可中断等待;持续为 T 则说明进程已暂停。单次快照不能证明趋势,间隔十秒再采一份更有价值。
先保留证据,再做清理。重启成功只能恢复工作,不能说明根因已经消失。
验证进程和调试会话
确认附加的是正确产物
同名应用可能同时存在于多个模拟器、测试目录或旧构建目录。先在 LLDB 中确认进程和已加载镜像:
(lldb) process status
(lldb) target list
(lldb) image list -o -f
target list 应指向本次构建的可执行文件;image list 中目标模块的路径应落在预期的 Build Products 目录。若路径来自另一套 DerivedData,不要立刻全量删除,先记下旧路径并检查 Scheme 是否复用了错误的构建产物。
若 LLDB 命令行本身仍可响应,可执行:
(lldb) thread list
(lldb) thread backtrace all
如果所有线程都有回溯,调试通道通常已经建立,问题更可能位于应用自身等待或符号解析。若命令持续无返回,再从系统侧采样目标进程:
sample <PID> 10 1 -file "$HOME/lldb-case/app-sample.txt"
不要连续运行多个 sample。十秒样本足以判断主线程是在锁、文件 I/O、网络调用还是系统框架中等待。
用 UUID 核对 dSYM
文件名相同不代表符号匹配。每次链接都可能产生不同 UUID,LLDB 只会采用与 Mach-O 对应的 dSYM。
dwarfdump --uuid "/path/to/MyApp.app/MyApp"
dwarfdump --uuid "/path/to/MyApp.app.dSYM"
两边相同架构的 UUID 必须一致。若应用包含 arm64,至少要对照 arm64 那一行;不要拿另一轮 Archive、另一提交或重新链接后的 dSYM 顶替。
检查模块是否已加载
在 LLDB 中查询目标模块:
(lldb) image lookup -n AppDelegate
(lldb) image lookup -r -n 'YourModule\..*'
(lldb) breakpoint list
image lookup 找不到符号,但 image list 已出现模块时,优先检查调试信息格式。开发构建通常应生成 DWARF 或 DWARF with dSYM;如果使用了高优化配置,函数可能被内联,局部变量也可能不可见。此时应建立一个专用诊断 Configuration,而不是临时修改团队共用的 Release 配置。
断点显示 pending 不一定是错误。动态框架尚未加载时,断点会等待对应镜像进入进程。应把断点设在确定已加载的入口,再观察后续模块,而不是反复删除同一个断点。
从系统日志找握手失败
Xcode 界面经常只显示“附加失败”,系统日志会保留更具体的进程退出、权限拒绝或连接中止信息。复现前开启一段受限日志采集:
log stream --style compact --info \
--predicate 'process == "debugserver" OR process == "lldb-rpc-server"' \
> "$HOME/lldb-case/debug-session.log"
复现一次后用 Control-C 停止。日志可能包含用户名、工程路径和设备标识,提交工单或共享给团队前应脱敏。不要用无谓的全系统 --debug 长时间采集,它会制造大量噪声,也会增加筛选成本。
若日志显示目标进程在附加瞬间退出,应先脱离 LLDB 单独启动应用,确认它能稳定存活。若只有测试进程失败,则分别检查测试宿主、测试 Bundle 和被测应用,不要只盯主应用 PID。
建立可重复的恢复顺序
恢复动作应从影响最小的步骤开始:
- 结束当前调试会话,但保留目标应用和日志。
- 核对 Scheme、Configuration、运行目标及可执行文件路径。
- 比较 Mach-O 与 dSYM UUID。
- 重新构建当前 Target,不执行全工程清理。
- 仅在确认旧产物被复用后,删除对应工程的 DerivedData 目录。
- 仍可复现时,保存线程回溯、进程采样、系统日志和最小复现步骤。
可用下面的命令先列出目录及更新时间,避免误删其他任务的数据:
find "$HOME/Library/Developer/Xcode/DerivedData" \
-maxdepth 1 -mindepth 1 -type d -print
云端 Mac 上可能同时运行构建、测试和图形调试任务。清理前应确认没有其他流水线正在使用同一目录;更稳妥的方案是让不同任务使用独立的 -derivedDataPath,把调试现场与自动构建缓存分开。
把排查结果变成验收项
一次修复结束后,至少完成四项验收:冷启动可附加、已有进程可附加、源码断点能命中、异常暂停时能查看关键变量。随后退出并重新进入远程会话再测一次,排除结果依赖当前图形会话或临时环境变量。
团队可以把 UUID 核对、产物路径和 Configuration 写入构建清单,但不要把个人目录写死进脚本。真正稳定的调试流程并非“清理后再试”,而是任何成员都能根据同一组证据判断故障位于进程、会话还是符号层,并只修复发生偏差的那一层。
常见问题
LLDB 能附加进程但断点始终不命中,应该先查什么?
先用 image list 确认目标模块已经加载,再核对可执行文件与 dSYM 的 UUID。模块未加载时断点会保持 pending,UUID 不一致时则无法获得正确的源码和行号。
调试卡住时可以立即清理 DerivedData 吗?
不建议。先保存进程列表、LLDB 输出、系统日志和当前产物 UUID;直接清理会破坏可复现现场。确认问题来自旧构建目录后,再只删除对应工程的目录。
远程调试需要把调试端口公开到互联网吗?
通常不需要。优先在云端 Mac 内部运行 Xcode 与 LLDB,并通过受控远程桌面或 SSH 操作;如必须转发端口,应使用受控隧道并限制监听地址。
需要一台由单一订单独享的物理 Mac mini?
核对 M4 与 M4 Pro 两档配置、四种租用周期和 5 个可选节点,再按工作流选择设备。