3、断点的高级用法:条件断点、符号断点、异常断点,以及如何通过断点快速定位崩溃源头

调试崩溃,说白了就是跟系统玩「谁先暴露破绽」的游戏。

我刚开始带团队时,有个同事天天被野指针折磨。他每次都在 viewDidLoad 里打一堆断点,然后一行一行看。我说你这不是调试,是体力活。真正高效的调试,是用断点「钓鱼」—— 你设好饵,等鱼自己上钩。

今天我就把三种最实用的断点用法掰开揉碎讲清楚。嗯,都是我在项目里摔过跟头才总结出来的。

3.1 条件断点:别让断点变成噪音

你想想看,一个 for 循环跑 1000 次,你每次都要手动点「继续」?那手指头不得抽筋?

条件断点就是干这个的 —— 只有满足你设定的条件时,断点才会触发。

核心用法:

  • 右键点击断点 → 选择「Edit Breakpoint」
  • 在 Condition 栏输入表达式(如 index == 500
  • 可选:在 Action 里添加调试命令,比如 po self.model

举个例子。我之前处理过一个数组越界崩溃,日志只告诉我崩溃在某个 for 循环里。我直接在循环行设断点,条件写成 i >= array.count。结果断点立刻停在了第 127 次循环 —— 数组只有 126 个元素。你看,一秒定位。

// 场景:遍历数组时偶发崩溃
for (NSInteger i = 0; i < dataArray.count; i++) {
    // 在这里设条件断点:i >= dataArray.count - 1
    // 或者更直接:po dataArray[i] 看看是不是 nil
    id item = dataArray[i];
    [item doSomething];
}

我的小技巧:条件断点里可以用 _cmdself。比如你想知道某个方法被谁调用了,条件写 _cmd == @selector(viewDidAppear:),然后勾上「Automatically continue after evaluating actions」,配合 bt 命令打印调用栈。这样断点不会停,但每次调用都会留下痕迹。

3.2 符号断点:精准打击,不拖泥带水

有时候你根本不知道崩溃在哪一行。比如系统方法报错,或者第三方库里的问题。这时候符号断点就派上用场了。

符号断点不是打在某个文件某一行,而是打在「某个方法被调用时」。不管这个方法在哪个类、哪个线程里被调用,断点都会触发。

设置方法:

  • 断点导航栏 → 左下角「+」→ Symbolic Breakpoint
  • Symbol 栏输入方法名,比如 -[NSArray objectAtIndex:]
  • 可以加 Module 限制(比如只监控 UIKit 框架)

我记得有一次,App 在启动时偶发崩溃,堆栈指向了 -[UIViewController viewDidLoad]。但所有控制器的 viewDidLoad 都会经过这里,根本不知道是哪个控制器出了问题。我设了一个符号断点:

Symbol: -[UIViewController viewDidLoad]
Condition: [self isKindOfClass:[MySpecificViewController class]]
Action: po self

断点一触发,我立刻看到了那个有问题的控制器。原来它里面有个 IBOutlet 没连上,viewDidLoad 里直接操作了 nil。

注意:符号断点不要滥用。如果你在 -[NSObject init] 上设断点,那 App 基本就卡死了 —— 因为每个对象创建都会触发。我建议加上 Module 限制,或者用 Condition 过滤掉大部分调用。

3.3 异常断点:崩溃现场的「监控摄像头」

异常断点是我个人最常用的,没有之一。它能在异常抛出的一瞬间就停住,而不是等到程序崩溃后才给你看堆栈。

为什么要用异常断点?因为默认情况下,Xcode 只会在 main 函数里捕获到异常,然后给你一个模糊的堆栈。你根本不知道异常是在哪一行抛出的。异常断点直接带你到「犯罪现场」。

设置方法:

  • 断点导航栏 → 左下角「+」→ Exception Breakpoint
  • Exception 选择「All」
  • Break 选择「On Throw」(重要!)

这里有个坑,我曾经踩过。默认的异常断点是「On Catch」,意思是异常被捕获时才停。但很多崩溃是未捕获异常,直接就被系统杀掉了。所以一定要改成「On Throw」—— 异常一被抛出,立刻停住。

// 异常断点停住后,你可以在 lldb 里做这些事:
// 1. po $arg1    -> 打印异常对象
// 2. po $arg2    -> 打印异常原因(如果是 objc_exception_throw)
// 3. bt          -> 打印完整调用栈
// 4. frame variable -> 查看当前栈帧的局部变量

避坑指南:我曾经遇到一个诡异问题 —— 异常断点总是停在系统框架里,根本看不到自己的代码。后来发现是某个第三方库在后台线程抛了一个异常,然后自己 catch 了。解决方案:在异常断点的 Action 里加一句 thread backtrace,然后勾上「Automatically continue」。这样断点不会停,但每次异常抛出都会在控制台打印调用栈。你翻一翻日志,就能找到自己的代码在哪。

3.4 实战:用断点组合拳定位崩溃

光说不练假把式。我拿一个真实案例演示一下怎么用这三种断点配合定位崩溃。

场景:App 在某个页面滑动时偶发崩溃,崩溃率大概 5%,无法稳定复现。

第一步:设异常断点

先设一个「On Throw」的异常断点。这样崩溃发生时,Xcode 会立刻停在异常抛出的位置。我看到了堆栈指向了 UITableView reloadData 内部。

第二步:加符号断点

因为异常断点停在了系统方法里,我看不到自己的代码。于是我在 -[UITableView reloadData] 上加了一个符号断点,条件写成 self == (那个有问题的 tableView)。这样每次 reloadData 时,断点都会触发,我就能看到是谁调用的。

第三步:条件断点收网

通过符号断点,我发现是某个数据回调里调用了 reloadData。但数据回调本身没问题,问题出在数据源数组被另一个线程修改了。我在数据源数组的 addObject:removeObject: 方法上设了条件断点,条件写成 [NSThread isMainThread] == NO。果然,有一个后台线程在偷偷修改数组。

总结一下我的调试流程:

  1. 异常断点 → 找到崩溃的「第一现场」
  2. 符号断点 → 追踪是谁触发了崩溃
  3. 条件断点 → 锁定具体的异常操作

说白了,断点就是你的「侦探工具」。异常断点告诉你「出事了」,符号断点告诉你「谁干的」,条件断点告诉你「在什么情况下干的」。三个配合起来,再隐蔽的崩溃也能揪出来。

最后提醒一句:调试完记得把断点删掉。我见过有人把异常断点留在项目里,结果上线后 App 在用户手机上频繁闪退 —— 因为异常断点虽然不会导致崩溃,但会影响性能。嗯,别问我怎么知道的。