Highlight
LLDB 将调试建模为 Run → Break → Inspect 的循环:每次暂停后检查状态,根据结果决定继续执行还是重新运行,逐步缩小问题范围直到定位 bug。
核心内容
print 调试是大多数人最先学会的调试方式:插入 print 语句,重新编译,运行,看输出,不满意就再来一轮。这个过程有三个问题——每次改 print 都要重新编译;print 语句容易遗留到生产环境;而且你能检查的状态完全取决于你事先想到了什么。LLDB 提供了一套更高效的替代方案:在运行时动态设断点、检查变量、计算表达式,全部不需要重新编译。
这场 Session 把调试抽象成一个搜索问题。程序在某一点出现了错误行为,bug 存在于程序启动到出错之间的某段代码中。你的目标就是找到那段代码。方法是在不同时间点检查程序状态,每次检查都让你离 bug 更近一步。LLDB 提供了四个核心工具来完成这个搜索:backtrace(回溯调用栈)、变量检查、断点、表达式求值。
Session 以 Destination Video 这个多平台视频播放 App 为示例,围绕一个真实的调试场景展开:开发者给 App 添加了 “Watch Later” 功能,需要理解 SwiftUI 闭包的执行时机,排查 JSON 解码错误,以及自定义数据类型在调试器中的显示格式。每种场景都对应 LLDB 的一个核心能力。
详细内容
crashlog 离线分析
程序崩溃时,Apple 平台会自动收集崩溃时刻的状态信息,生成 crashlog。LLDB 可以直接消费 crashlog,模拟出一个调试会话——不需要运行程序就能定位问题(04:37)。
在 Xcode 中右键点击 crashlog 文件,选择用 Xcode 打开并关联到对应项目。Xcode 会用 LLDB 创建一个调试会话,高亮崩溃行。通过 backtrace 可以查看完整的调用链:当前帧是 JSON 加载函数,上层帧显示正在导入视频元数据,再往上是程序初始化。一条 crashlog 有时就足够定位 bug。
前提条件:项目需要 checkout 到与 crashlog 相同的 commit,并且对应的 dSYM bundle 可用。
SwiftUI 中的一行断点解析为多个位置
在 SwiftUI 的 Button 构造函数上设一个行断点,LLDB 会将其解析为多个独立的断点位置(08:29):
Button(action: { watchLater.toggle(video: video) }) {
let inList = watchLater.isInList(video: video)
Label(inList ? "In Watch Later" : "Add to Watch Later",
systemImage: inList ? "checkmark" : "plus")
}
关键点:
- LLDB 将这一行解析为 3 个断点位置,ID 分别为 1.1、1.2、1.3
- 1.1 是
Button构造函数的调用点,在创建 UI 时触发 - 1.2 是 action 闭包(
watchLater.toggle(video: video)),只在用户点击按钮时触发 - 1.3 是 trailing closure(label 闭包),由构造函数自身调用
- 这是 SwiftUI 等声明式框架的常见场景:同一行代码通过不同代码路径执行
- 用
breakpoint list命令可以查看每个位置的行号和列号 - 每个位置可以单独启用或禁用
断点动作与命令行操作
在 Break/Inspect 循环中,反复手动执行相同命令很繁琐。断点动作(breakpoint actions)可以在命中断点时自动执行命令(13:17):
p "last video is \(watchLater.last?.name)"
在 Xcode 中右键断点 → Edit Breakpoint → 添加 Debugger Command action,还可以勾选 “Continue after evaluation” 让程序自动继续执行。
在命令行中同样可以操作(14:42):
b DetailView.swift:70
break command add
p "last video is \(watchLater.last?.name)"
continue
DONE
关键点:
b是breakpoint set的简写break command add为最近一个断点添加动作,输入DONE结束- 命令行设置的动作会覆盖 Xcode 中设置的动作
help和apropos命令可以搜索 LLDB 的所有命令
条件断点与高频断点的处理
当断点在循环中被触发太多次时,有三种过滤手段(16:09):
- 条件断点:用
break modify --condition添加布尔表达式,如video.duration > 60 - 临时断点(tbreak):在断点动作中用
tbreak创建只触发一次的断点,适合”从 A 执行到 B 才停下来”的场景 - 忽略计数:
break modify --ignore-count 10跳过前 N 次命中
当一行代码执行百万次时,上述方法仍会导致 LLDB 每次都停下来判断条件,拖慢执行速度。这时建议在代码中加 if 判断,内部调用 raise(SIGSTOP) 让调试器接管。
p 命令与表达式求值
从 Xcode 15 开始,p 被重新定义为 “do what I mean” print 的别名,覆盖了大多数变量检查和表达式求值场景(19:57)。
在排查 JSON 解码错误时,可以用 p 逐步构建复杂表达式(24:06):
p watchLater.count
p watchLater.last!.name
关键点:
p可以在任何 backtrace 帧中求值表达式- 可以逐步构建表达式,每步查看中间结果,不需要重新编译
- 变量也可以在 Xcode 的变量查看器或源码悬停提示中查看
- 部分类型(如 URL、Image)有 Quick Look 按钮,提供额外可视化
- Swift Error breakpoint 可以在 Swift Error 抛出时自动暂停程序
@DebugDescription 宏
Swift 6 引入了 @DebugDescription 宏,让自定义类型在调试器中显示可读摘要(26:46):
@DebugDescription
struct WatchLaterItem {
let video: Video
let name: String
let addedOn: Date
var debugDescription: String {
"\(name) - \(addedOn)"
}
}
关键点:
- 用
@DebugDescription标注类型,再提供debugDescription计算属性 debugDescription必须使用字符串插值和存储属性构建- 标注后,变量查看器和
p命令中会直接显示摘要,不需要手动展开每个条目 - 如果你之前用
CustomDebugStringConvertible协议配合po命令,且实现只用了字符串插值和计算属性,可以迁移到@DebugDescription,与调试器集成更好
核心启发
- 做什么:用断点动作替代 print 调试。为什么值得做:断点动作不需要重新编译,可以动态添加和修改,避免了 print 语句遗留到生产环境的风险。怎么开始:在 Xcode 中右键断点 → Edit Breakpoint → 添加 Debugger Command action,输入
p表达式并勾选自动继续。 - 做什么:为项目中频繁出现在集合中的核心数据类型添加
@DebugDescription。为什么值得做:调试时在变量查看器中直接看到每条记录的摘要,而不是逐个展开,大幅提升调试效率。怎么开始:用@DebugDescription标注类型,实现debugDescription计算属性,用字符串插值输出关键信息。 - 做什么:在 CI 流水线中归档每个构建的 dSYM bundle。为什么值得做:crashlog 离线分析依赖 dSYM 进行符号化,没有 dSYM 就只能看到内存地址,无法定位到源码行号。怎么开始:确认 Xcode Archive 时 dSYM 生成选项已开启,App Store Connect 会自动处理上传的 dSYM,内部测试分发需要自行归档管理。
- 做什么:用条件断点和忽略计数处理高频断点,而非放弃设断点。为什么值得做:循环中的断点可能触发成千上万次,手动点继续不现实;条件断点只在满足条件时暂停,忽略计数跳过已知无关的迭代,两者结合能精准定位目标。怎么开始:右键断点 → Edit Breakpoint → 填写 Condition 或 Ignore Count,命令行用
break modify --condition或--ignore-count。
关联 Session
- Symbolication: Beyond the basics — 深入 dSYM 符号化,让 crashlog 显示正确的源码行号
- Debug with structured logging —
p命令底层机制与结构化日志的最佳实践 - What’s new in Swift — Swift 6 新特性总览,包括 @DebugDescription 等宏
- Analyze heap memory — 用 Instruments 分析堆内存,定位内存泄漏与性能瓶颈
评论
GitHub Issues · utterances