iOS crash 报告分析系列 - 语言异常崩溃

902

什么是语言异常呢?简单来说就是由于不规范编写代码造成的问题称之为语言异常。

比如数组越界、调用某个类未实现的方法等等。那么如何通过看崩溃报告来确定崩溃是否由语言异常导致的呢?首先要看的就是 Exception information 这一栏是否遵循下面的范式:

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Triggered by Thread:  0

还有就是,当崩溃是由于未捕获的语言异常导致的时候,崩溃报告中肯定会包含 Last Exception Backtrace 这一栏信息:

截屏2023-06-05 14.26.54.png

下面,我们通过详细分析Last Exception Backtrace 这一栏信息,来去定位是什么原因导致的崩溃。

Last Exception Backtrace 分析

在这一栏,操作系统通常会记录关于当前崩溃的完整函数调用栈。该栏的回溯会以明确抛出语言异常的帧结尾。

在函数回溯的过程中,你会发现有关抛出异常的方法的关键信息,以及代码的哪个部分调用了引发异常的方法。比如下面的例子:

崩溃代码:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSArray *arr = @[];
    NSLog(@"%d", arr[5]);

}
Last Exception Backtrace:
0   CoreFoundation                	       0x191560e38 __exceptionPreprocess + 164
1   libobjc.A.dylib               	       0x18a6f78d8 objc_exception_throw + 60
2   CoreFoundation                	       0x191672b48 __CFArrayHash + 0
3   CrashDemo                     	       0x1027d5e18 -[ViewController touchesBegan:withEvent:] + 116
4   UIKitCore                     	       0x19392ddfc forwardTouchMethod + 284
5   UIKitCore                     	       0x1938281b0 -[UIWindow _sendTouchesForEvent:] + 356
6   UIKitCore                     	       0x193827770 -[UIWindow sendEvent:] + 3284
7   UIKitCore                     	       0x193826a20 -[UIApplication sendEvent:] + 676
8   UIKitCore                     	       0x1938260d8 __dispatchPreprocessedEventFromEventQueue + 7084
9   UIKitCore                     	       0x19386de00 __processEventQueue + 5632
10  UIKitCore                     	       0x1944cb820 updateCycleEntry + 168
11  UIKitCore                     	       0x193d7e5b0 _UIUpdateSequenceRun + 84
12  UIKitCore                     	       0x1943cd310 schedulerStepScheduledMainSection + 172
13  UIKitCore                     	       0x1943cc4dc runloopSourceCallback + 92
14  CoreFoundation                	       0x19162cf24 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 28
15  CoreFoundation                	       0x1916392fc __CFRunLoopDoSource0 + 176
16  CoreFoundation                	       0x1915bd1c0 __CFRunLoopDoSources0 + 244
17  CoreFoundation                	       0x1915d2b7c __CFRunLoopRun + 836
18  CoreFoundation                	       0x1915d7eb0 CFRunLoopRunSpecific + 612
19  GraphicsServices              	       0x1cb7cd368 GSEventRunModal + 164
20  UIKitCore                     	       0x193acd668 -[UIApplication _run] + 888
21  UIKitCore                     	       0x193acd2cc UIApplicationMain + 340
22  CrashDemo                     	       0x1027d60b8 main + 120
23  dyld                          	       0x1afed0960 start + 2528

报告解读: 首先,第 0 - 1 帧可以看出是操作系统抛出了异常。第 2 帧可以分析出当前异常是与 NSArray 有关的。第 3 帧可以看出是 ViewControllertouchesBegan:withEvent: 调用导致了该崩溃的发生。定位到这,我们就可以去相应的代码那,去具体分析到底是数组的什么操作导致了该崩溃的发生。

第 4 - 23 帧则是 app 启动和事件转发的过程,跟崩溃原因调查关系不大。

Tips:如果 API 抛出 doesNotRecognizeSelector(_:) 的异常,那么这个异常可能是由于僵尸对象造成的。如何分析可以看这里

查看异常信息

操作系统提供的未捕获异常处理程序在终止进程之前,会将异常消息记录到控制台。所以,如果你能复现,你会在控制台看到如下的打印:

***** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArray0 objectAtIndex:]: index 5 beyond bounds for empty NSArray'**

这个信息就可以很明显的看出是由于数组越界而导致的崩溃了。

那为什么崩溃报告不包含这么简单明了的日志呢?下面是 Apple 的回答: iOS、iPadOS、watchOS 和 tvOS 崩溃报告不包含异常消息,以防止通过异常消息泄露有关用户的私人信息。

分析系统语言异常引起的崩溃

在确定是操作系统的 API 抛出异常后,我们应该查阅该 API 的文档来确定触发异常的条件。还尝试使用 Xcode 调试器去重现崩溃,以在控制台中获取有关异常的其他信息。最后,可以使用回溯中的帧作为需要测试的特定代码的方向。

如果无法重现崩溃的话,就需要使用所有的线程回溯(不仅仅是异常回溯)作为线索,去分析当崩溃发生时,程序到底在做什么。