1、崩溃定位基础:认识Xcode的崩溃日志,区分EXC_BAD_ACCESS、SIGABRT、NSInternalInconsistencyException等常见崩溃类型

崩溃日志——你的第一道防线

说实话,我刚入行那会儿,看到崩溃日志就头大。满屏的十六进制地址,一堆看不懂的符号,感觉像在看天书。后来被虐多了,才慢慢摸出门道。

崩溃日志其实就三块核心信息:异常类型异常代码调用栈。你只要抓住这三个点,定位问题就成功了一半。

崩溃日志的黄金三要素:

  • 异常类型(Exception Type):告诉你崩溃的「罪名」
  • 异常代码(Exception Code):告诉你崩溃的「细节」
  • 调用栈(Call Stack):告诉你崩溃的「案发现场」

我个人习惯,拿到崩溃日志先看异常类型。不同的类型,排查思路完全不同。下面我带你逐个过一遍最常见的三种。

EXC_BAD_ACCESS——野指针的噩梦

这个崩溃,我估计每个iOS开发者都遇到过。说白了就是:你访问了一块已经释放的内存

为什么会这样?最常见的情况是:你持有一个对象的弱引用,或者对象被提前释放了,但你还拿着它的指针在操作。

我曾经踩过的坑:

有一次我在一个异步回调里访问了一个UIViewController的属性。当时觉得没问题,结果用户快速返回上一页时,控制器被释放了,回调还在执行。啪,EXC_BAD_ACCESS。后来我学乖了,异步回调里一律用weakSelf + 判空。

EXC_BAD_ACCESS 的异常代码通常长这样:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Code: KERN_INVALID_ADDRESS at 0x0000000000000010

看到 KERN_INVALID_ADDRESS 了吗?这表示你访问的地址根本不存在。地址是0x10这种小数字,基本可以确定是野指针——对象被释放后,内存被清零了。

调试技巧:

开启 Zombie Objects(僵尸对象)检测。在 Xcode 的 Scheme 设置里,Diagnostics 选项卡勾上。这样访问已释放对象时,Xcode 会直接告诉你「这个对象曾经是什么类型」,而不是给你一个冰冷的十六进制地址。

SIGABRT——系统主动叫停

SIGABRT 和 EXC_BAD_ACCESS 不一样。它不是内存访问违规,而是系统或运行时主动调用了 abort() 函数

你想想看,什么时候系统会主动终止你的App?通常是某个断言失败了,或者某个不可恢复的异常被抛出了。

最常见的场景:

  • unrecognized selector sent to instance —— 你给一个对象发了它不认识的消息
  • NSArray / NSDictionary 越界或插入 nil —— 集合操作不规范
  • KVO 重复添加或移除监听 —— 观察者模式用错了
Exception Type:  SIGABRT
Exception Subtype: NSInvalidArgumentException
Reason: -[__NSArrayM objectAtIndex:]: index 5 beyond bounds [0 .. 3]

看到没?SIGABRT 的日志里通常会有明确的 Reason 字段。这是你的好朋友,直接告诉你「数组越界了」。相比 EXC_BAD_ACCESS 那种模糊的地址,SIGABRT 好定位得多。

嗯,这里要注意:SIGABRT 的调用栈里,最上面的几行通常是系统框架的代码,别被吓到。往下翻,找到你自己的代码,那才是问题所在。

NSInternalInconsistencyException——内部状态不一致

这个异常,说白了就是你破坏了某个类内部的不变式。系统说:「我内部的状态被你搞乱了,没法继续了,直接崩吧。」

我在项目中遇到过最典型的例子是 UITableView 的数据源不一致。比如你告诉 tableView 有10行,但实际返回的 cell 数量只有8个。系统一算,对不上,直接抛 NSInternalInconsistencyException。

Exception Type:  NSInternalInconsistencyException
Reason: Invalid update: invalid number of rows in section 0. 
The number of rows contained in an existing section after the update (10) 
must be equal to the number of rows contained in that section before the update (8)

这种崩溃的排查思路很明确:检查数据源方法返回的值是否和实际数据一致。尤其是做批量增删操作时,beginUpdates 和 endUpdates 之间的数据变化一定要同步好。

三种崩溃类型速查表:

崩溃类型 本质原因 典型特征 排查方向
EXC_BAD_ACCESS 访问已释放内存 KERN_INVALID_ADDRESS 开启Zombie,检查内存管理
SIGABRT 系统主动终止 有明确的Reason字段 看Reason,检查断言和异常
NSInternalInconsistencyException 内部状态不一致 数据源不匹配、约束冲突 检查数据源一致性

实战:从崩溃日志到修复

光说不练假把式。我给你一个真实的崩溃日志片段,咱们一起分析一下:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Code: KERN_INVALID_ADDRESS at 0x0000000000000018
...
Thread 0 Crashed:
0   libobjc.A.dylib                 objc_msgSend + 16
1   UIKitCore                       -[UIView(UIViewGeometry) frame] + 64
2   MyApp                           -[MyViewController viewDidAppear:] + 120
3   UIKitCore                       -[UIViewController _setViewAppearState:isAnimating:] + 680

第一步,看异常类型:EXC_BAD_ACCESS,内存问题。

第二步,看崩溃线程:Thread 0,主线程。

第三步,看调用栈:崩溃在 objc_msgSend,说明是在给某个对象发消息时出事了。往上翻,看到 MyViewControllerviewDidAppear: 方法里调用了 frame 属性。

嗯,到这里基本可以判断:MyViewController 里某个 UIView 被提前释放了,但 viewDidAppear: 还在访问它的 frame。打开 Zombie 一查,果然是一个被 weak 引用的子视图在页面切换时被回收了。

我的个人经验:

遇到 EXC_BAD_ACCESS 别慌。先看调用栈里有没有你自己的代码,有的话直接定位到那一行。如果没有,就往上找,看哪个系统方法调用了你的代码。实在不行,开启 Address Sanitizer(地址消毒剂),它能精确告诉你哪块内存被非法访问了。

小结

崩溃定位其实就三步:看类型、读原因、查栈帧。EXC_BAD_ACCESS 是内存问题,SIGABRT 是逻辑问题,NSInternalInconsistencyException 是状态不一致问题。每种类型都有对应的排查工具和思路。

记住,崩溃日志不是天书,它是你的破案线索。多读几次,你就能一眼看出问题在哪。下一章我会讲怎么用 LLDB 在崩溃现场做「尸检」,那才是真正的高效调试。