LLDB调试入门:掌握po、p、expr、bt等基础命令
说实话,我刚接触iOS开发那会儿,调试全靠NSLog。满屏幕的打印日志,找问题跟大海捞针似的。后来一位老同事跟我说:「你试试LLDB,比NSLog好用一百倍。」我试了一下,嗯,真香。
LLDB是Xcode默认的调试器。说白了,它就是你跟App运行时对话的窗口。你问它「这个变量是什么?」,它告诉你。你问它「执行到这里时发生了什么?」,它也告诉你。今天我就带你入门几个最常用的命令。
1. p 命令:打印基础类型
p 是 print 的缩写。它用来打印变量的值。用法很简单:
(lldb) p count
(int) $0 = 42
(lldb) p userName
(NSString *) $1 = 0x0000000282e3a040 @"Alice"
你看,它不光打印值,还告诉你类型和内存地址。我个人习惯用 p 来快速确认基本类型变量的状态。比如检查一个整数是不是预期值,或者一个布尔值是不是 true。
p 会自动创建一个临时变量(比如 $0、$1),你可以在后续命令中直接引用它。比如 p $0 + 10 会输出 52。
2. po 命令:打印对象描述
po 是 print object 的缩写。它调用对象的 description 方法,打印出更友好的信息。
(lldb) po user
<User: 0x600000234a80>
(lldb) po user.name
Alice
(lldb) po user.age
30
我在项目中遇到过一个问题:用 p 打印一个自定义对象,只看到内存地址,完全不知道里面有什么。换成 po 之后,对象的属性一目了然。所以对于 Objective-C 对象或 Swift 类实例,我几乎只用 po。
description 方法没有实现,po 也只能打印内存地址。这时候你需要自己实现 description 或 debugDescription。
3. expr 命令:执行表达式
expr 是 expression 的缩写。它可以在调试过程中执行任意代码。这个功能非常强大。
(lldb) expr count = 100
(int) $2 = 100
(lldb) expr [self reloadData]
(void) $3 = void
(lldb) expr self.view.backgroundColor = [UIColor redColor]
(void) $4 = void
你想想看,断点停下来之后,你发现某个变量值不对。不用重新运行,直接 expr 改掉它,然后继续执行。我曾经用这个技巧在调试界面布局时,实时修改颜色和位置,省去了无数次重新编译的时间。
- 修改变量值:
expr variableName = newValue - 调用方法:
expr [object method] - 创建新对象:
expr NSArray *arr = @[@1, @2]
4. bt 命令:查看调用栈
bt 是 backtrace 的缩写。当程序崩溃或停在断点时,它告诉你「你是怎么走到这一步的」。
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000100a3b4c0 MyApp`ViewController.viewDidLoad(self=0x0000000282e3a040) at ViewController.m:15
frame #1: 0x00000001a2b3c4d0 UIKitCore`-[UIViewController loadViewIfRequired] + 1234
frame #2: 0x00000001a2b3e5f0 UIKitCore`-[UIViewController view] + 56
frame #3: 0x00000001a2b4a7b0 UIKitCore`-[UIWindow addRootViewControllerViewIfPossible] + 789
嗯,这里要注意:bt 显示的是从当前函数一直追溯到入口的调用链。最上面是当前停住的位置,越往下越早。我遇到崩溃时,第一件事就是打 bt,看看崩溃前调用了哪些方法。很多时候,问题就出在某个调用链上。
bt 10 只显示最近的 10 帧。或者用 frame select 3 跳转到第 3 帧的上下文。
5. 组合使用:实战案例
光说理论没意思。我给你讲个真实场景。
有一次,我的 App 在某个页面点击按钮后闪退。我加了个断点,然后一步步调试:
- 先用
bt查看调用栈,发现是从一个网络回调里调用了 UI 更新。 - 用
po self确认当前对象没有被释放。 - 用
p isMainThread检查是否在主线程——结果是false。 - 用
expr dispatch_async(dispatch_get_main_queue(), ^{ [self updateUI]; })临时修复。
整个过程不到两分钟。要是用 NSLog,我得先加日志、重新运行、复现问题、看日志、再改代码……至少折腾十分钟。
6. 常用命令速查表
| 命令 | 全称 | 用途 | 示例 |
|---|---|---|---|
| p | 打印基本类型值 | p count |
|
| po | print object | 打印对象描述 | po user.name |
| expr | expression | 执行任意代码 | expr count = 10 |
| bt | backtrace | 查看调用栈 | bt |
| frame | frame | 切换栈帧 | frame select 2 |
| thread | thread | 查看线程信息 | thread list |
7. 避坑指南
- 在 Swift 中使用
p打印可选类型时,会显示Optional(42)。用po会直接显示42,更干净。 expr修改属性时,如果对象已经被释放,会 crash。先po self确认对象还在。- 在汇编级别调试时,
bt可能显示不全。试试bt all查看所有线程的调用栈。
好了,这几个命令够你应付 80% 的调试场景了。下次遇到崩溃,别急着加 NSLog,先试试 LLDB。你会发现,调试原来可以这么爽。
公众号:蓝海资料掘金营,微信deep3321