LLDB调试入门:掌握po、p、expr、bt等基础命令

说实话,我刚接触iOS开发那会儿,调试全靠NSLog。满屏幕的打印日志,找问题跟大海捞针似的。后来一位老同事跟我说:「你试试LLDB,比NSLog好用一百倍。」我试了一下,嗯,真香。

LLDB是Xcode默认的调试器。说白了,它就是你跟App运行时对话的窗口。你问它「这个变量是什么?」,它告诉你。你问它「执行到这里时发生了什么?」,它也告诉你。今天我就带你入门几个最常用的命令。

1. p 命令:打印基础类型

pprint 的缩写。它用来打印变量的值。用法很简单:

(lldb) p count
(int) $0 = 42

(lldb) p userName
(NSString *) $1 = 0x0000000282e3a040 @"Alice"

你看,它不光打印值,还告诉你类型和内存地址。我个人习惯用 p 来快速确认基本类型变量的状态。比如检查一个整数是不是预期值,或者一个布尔值是不是 true。

小技巧: p 会自动创建一个临时变量(比如 $0、$1),你可以在后续命令中直接引用它。比如 p $0 + 10 会输出 52。

2. po 命令:打印对象描述

poprint 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 也只能打印内存地址。这时候你需要自己实现 descriptiondebugDescription

3. expr 命令:执行表达式

exprexpression 的缩写。它可以在调试过程中执行任意代码。这个功能非常强大。

(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 命令:查看调用栈

btbacktrace 的缩写。当程序崩溃或停在断点时,它告诉你「你是怎么走到这一步的」。

(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 在某个页面点击按钮后闪退。我加了个断点,然后一步步调试:

  1. 先用 bt 查看调用栈,发现是从一个网络回调里调用了 UI 更新。
  2. po self 确认当前对象没有被释放。
  3. p isMainThread 检查是否在主线程——结果是 false
  4. expr dispatch_async(dispatch_get_main_queue(), ^{ [self updateUI]; }) 临时修复。

整个过程不到两分钟。要是用 NSLog,我得先加日志、重新运行、复现问题、看日志、再改代码……至少折腾十分钟。

6. 常用命令速查表

命令 全称 用途 示例
p print 打印基本类型值 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