0%

App 常见崩溃问题分析

前言

此文是基于这些年工作中项目里面常见崩溃的一些总结,整理出来方便查阅,希望对大家都有所帮助。

App 常见崩溃

  1. 数组下标越界
  2. 字典构造与修改
  3. NSAttributedString 相关
  4. 呈现一个空控制器
  5. unrecognized selector
  6. 操作 tableView 数据
  7. Push 到同一个控制器多次

1.数组下标越界

示例代码

1
2
3
4
5
- (void)testArrayOutOfBounds
{
NSArray *testArray = @[@1,@2,@3];
NSNumber *num = testArray[3];
}

异常现象

Terminating app due to uncaught exception ‘NSRangeException’, reason: ‘** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]’*

预防方案

在数组中取值时需要先进组下标索引边界检查,如果没有越界方可取值。

2.字典构造造与修改

示例代码

1
2
3
4
5
6
7
8
- (void)testDicSetNilValueCrash
{
// 构造不可变字典时 key 和 value 都不能为空
NSString *nilValue = nil;
NSString *nilKey = nil;
NSDictionary *dic1 = @{@"key" : nilValue};
NSDictionary *dic2 = @{nilKey : @"value"};
}

异常现象

Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]’*

预防方案

在我们使用字面量快速创建一个字典的时候需要特别小心,因为很可能字典的键和值不能保证同时不为空。有潜在崩溃的风险,这种崩溃非常容易出现,需要特别小心,但是当你留心的话也非常好避免,就是设置字典的键或者值的时候判断是否非空,可变字典设置某个键的值是可以为空,相当于删除字典中的某个键值。为了使 App 保持健壮推荐使用 KVO 或者字面量的方式来设置字典的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testMutableDicSetNilValueCrash
{
NSString *value = nil;
NSMutableDictionary *mDic = [NSMutableDictionary dictionary];

// via Dic set, leading crash
[mDic setObject:value forKey:@"key"];

// via KVO set, it's safe
[mDic setValue:value forKey:@"key"];

// or via literal set, it's safe
mDic[@"key"] = value;
}

3.NSAttributedString 相关

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testAttributedStringInitCrash
{
NSString *nilStr = nil;
NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:nilStr];
}

- (void)testAttributedStringAddAttributeCrash
{
NSString *nonnullStr = @"str";
NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:nonnullStr];

NSString *nilValue = nil;
[attributedStr addAttribute:NSAttachmentAttributeName value:nilValue range:NSMakeRange(0, 1)];
}

异常现象

Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘NSConcreteMutableAttributedString initWithString:: nil value’

预防方案

在构造 NSMutableAttributedString 或者 NSAttributedString 需要留意,设置的属性的值是否有可能存在 nil 的情况。这个很容易被人忽视,值得注意。

4.呈现一个空控制器

示例代码

1
2
3
4
5
6
7
- (void)testPresentNilControllerCrash
{
UIViewController *someVC = [UIViewController new];
UIViewController *presentVC = nil;

[someVC presentViewController:presentVC animated:YES completion:nil];
}

异常现象

present 一个空的控制器导致 App crash

预防方案

present 一个新控制器时,判断是否存在,存在才执行,否则直接返回

1
2
3
4
5
6
7
8
9
- (void)testPresentNilControllerCrashFixed
{
UIViewController *someVC = [UIViewController new];
UIViewController *presentVC = [UIViewController new];

if (presentVC) {
[someVC presentViewController:presentVC animated:YES completion:nil];
}
}

5.unrecognized selector

示例代码

1
2
3
4
- (void)testUnrecogernizedSelectorCash
{
[self performSelector:@selector(testSel) withObject:nil afterDelay:0];
}

异常现象

Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[ViewController testSel]: unrecognized selector sent to instance 0x7ffd41609d10’

预防方案

此类崩溃经常出现,特别是当服务器数据放回异常时,比如本来应该返回一个 NSString 类型字符串,结果返回 NULL ,当你调用字符串的 length 方式时,导致 App 崩溃。预防方法,重要的地方对类型进行判断再调用该类的相关方法,或者写一个分类统一处理此类逻辑。

6.操作 tableView 数据

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)testTableViewUpdateCrash
{
NSIndexPath *insertIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForRow:1 inSection:0];
NSIndexPath *reloadIndexPath = [NSIndexPath indexPathForRow:2 inSection:0];
NSIndexPath *moved1IndexPath = [NSIndexPath indexPathForRow:3 inSection:0];
NSIndexPath *moved2IndexPath = [NSIndexPath indexPathForRow:4 inSection:0];
[self.tableView beginUpdates];

[self.tableView insertRowsAtIndexPaths:@[insertIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView deleteRowsAtIndexPaths:@[deleteIndexPath]withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView reloadRowsAtIndexPaths:@[reloadIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView moveRowAtIndexPath:moved1IndexPath toIndexPath:moved2IndexPath];

[self.tableView endUpdates];
}

异常现象

Fatal Exception: NSInternalInconsistencyException

Invalid update: invalid number of sections. The number of sections contained in the table view after the update (1) must be equal to the number of sections contained in the table view before the update (1), plus or minus the number of sections inserted or deleted (1 inserted, 0 deleted).

预防方案

当需要动态更新 tableView 的数据时,计算好模型的数据使模型的数据和更新 tableView 的后的数据保持同步。

7.Push 到同一个控制器多次

异常现象

Fatal Exception: NSInvalidArgumentException

Pushing the same view controller instance more than once is not supported (<PPSelectPayMethodViewControllerIOS7: 0x10d7e7f10>)

参考链接

以上就是工作中常见的异常崩溃以及处理方案,下面的异常分类内容来自 Apple 的官方文档,有兴趣的可以查阅。☕️

Apple 官方常见异常类型(Exception types)

  1. 访问一块坏内存 [EXC_BAD_ACCESS] // SIGSEGV // SIGBUS] 2. 异常退出 [EXC_CRASH // SIGABRT] 3. 追踪受限 [EXC_BREAKPOINT // SIGTRAP] 4. 非法指令 [EXC_BAD_INSTRUCTION // SIGILL] 5. 被保护的资源遭到侵害 [EXC_GUARD] 6. 资源限制 [EXC_RESOURCE] 7. 其他异常类型

1.访问一块坏内存(Bad Memory Access)

当程序试图接入无效内容或者尝试以不被允许的方式接入由于内存的保护等级(例如,尝试写入只读的内存)。 Exception Subtype 字段包含一个kern_return_t结构体用来描述错误和不正确接入的内存地址。

下面是一些调试坏内存接入导致崩溃的建议:

  • 假如objc_msgSend或者objc_release在崩溃线程回溯(Backtraces)的顶部附近,这个线程可能尝试给一个释放的对象发消息。你应该 profile 应用使用Zombies instrument来更好的理解这个崩溃发生的条件。
  • 假如gpus_ReturnNotPermittedKillClient在崩溃线程回溯(Backtraces)的顶部附近,线程被终结因为它尝试用OpenGL ES或者Metal执行渲染当程序处于后台时。查看QA1766: How to fix OpenGL ES application crashes when moving to the background
  • 打开 Address Sanitizer 运行你的应用。Address Sanitizer 添加了额外的说明在内容接入当你编译代码的时候。随着你应用的运行,Xcode 将⚠️你假如内存以一种可能导致崩溃的方式接入。

2.异常退出(Abnormal Exit)

程序异常退出,是最常见导致这类异常崩溃的原因是捕获到 Objective-C/C++ 异常和调用了 abort() 函数。

App Extensions将被终结发生这种类型的异常,假如他们初始化花费太多的时间(watchdog终结)。假如一个extension由于载入时间太长被终结,产生崩溃报告的Exception Subtype将是LAUNCH_HANG。因为extensions并没有一个 main 函数,任何花销在初始化的时间都发生在static constructors和呈现在你的extensions和依赖库的 +load 方法。你应该尽可能的延迟做这些工作。

3.追踪受限(Trace Trap)

和异常退出类似,这种异常的目的是给一个追加的调试器,让它有机会来打断在一个当它执行时候指定的点的进程。你可以使用 __builtin_trap() 函数在你的代码中来触发这个异常。假如没有调试器追加的话,线程将被终结并且产生一个崩溃报告。

低等级的库(例如libdispatch)将受限这个进程一旦遇到一个重大的错误。关于错误的额外信息可以在Additional Diagnostic Information章节中的崩溃报告找到,或者在设备的控制台。

假如在 runtime 遇到诸如下面的一个意外的条件, Swift 代码将终结出现这种类型的异常:

  • 非可选类型带有一个 nil
  • 错误的强制类型转换

查看Backtraces来决发生定异常条件的位置。额外的信息可能已经在设备的控制台打印出来了。你应该修改崩溃处的代码来优雅的处理 runtime 错误。例如,使用Optional Binding而不是强制解包一个可选变量。

4.非法指令(Illegal Instruction)

进程尝试执行一个非法或者未定义的指令。进程可能已经尝试跳进到一个无效的地址通过一个配置错误的函数指针。在 Intel 处理器中, ud2 操作码导致一个EXC_BAD_INSTRUCTION异常,但是它通常被用来困住进程达到调试的目的。 Swift 代码在 Intel 处理器中将以这种异常终结,假如在 runtime 位置条件发生。更多详情查看Trace Trap

5.被保护的资源遭到侵害(Guarded Resource Violation)

进程侵犯一个被保护的资源。系统库可能某个文件的描述器成 guarded ,在那以后,所有不正常的操作在这些描述器上都将触发一个EXC_GUARD异常(当它想操作在这些文件描述器上,系统可以使用特殊的 guarded 标记的私有 APIs)。这可以帮你向下快速追踪问题,例如关闭一个已经被系库打开的文件描述器。例如,假如一个 app 关闭文件秒杀器通过使用截图 SQLite 文件到一个 Core Data 存储, Core Data 将会在随后诡异的崩溃。guard exception将让这些问题尽早引起你的注意,这样也让他们变得更容易调试。

崩溃报告来自新版的 iOS 包含了人类可读的详细信息关于引起 EXC_GUARD 异常的操作在 Exception SubtypeException Message 字段中。在来自 macOS 或者老版本的 iOS 的崩溃报告中,这些信息被编码到第一个 Exception Code 就像一个分解成如下的位段:

  • [63:61] - Guard Type:被保护的资源类型。 0x2 代表一个文件描述器资源。
  • [60:32] - Flavor:侵害被处罚时的条件
    • 假如第一个 (1 << 0) 位被设置,进程尝试执行 close() 函数在一个受保护的文件描述器。
    • 假如第二个 (1 << 1) 位被设置,进程尝试执行 dup()dup2() ,或者 fcntl()F_DUPFD 或者 F_DUPFD_CLOEXEC 命令在一个受保护的文件描述器。
    • 假如第三个 (1 << 2) 位被设置,进程尝试通过一个 socket 发送给一个受保护的文件描述器。
    • 假如第三个 (1 << 3) 位被设置,进程尝试写入到一个受保护的文件描述器。
  • [31:0] - File Descriptor:进程尝试修改的受保护的文件描述器。

6.资源限制(Resource Limit)

进程超出了一个资源消耗的限制。这是一个来自操作系统通知,告诉进程正在使用的资源过多。精确的资源列在 Exception Subtype 字段中。假如 Exception Note 字段包含 NON-FATAL CONDITION ,进程不会被终结即使产生了一个崩溃报告。

  • 异常子类型 MEMORY 表明进程已经越过系统应用的内存限制。这可能是一个终结的先兆由于超额的使用内存。

  • 异常子类型 WAKEUPS 表明在进程中的线程每秒被唤醒太多次,这强制 CPU 非常频繁的唤醒消耗电池寿命。

    典型的,这个通过由线程与线程的通信产生(通常是使用 peformSelector:onThread:dispatch_async ),那样无意的发生了远远超出它正常应该的切换频率。因为通信的协调发生得非常频繁而出发此类的异常,这个通常和多个后台线程有着相似 Backtraces – 表明那些地方发生过通信。

7.其他异常类型(Other Exception Types)

一些崩溃报告可能含有一个未命名的 Exception Type ,将以一个 16 进制的值(例如,00000020)的形式打印。假如你的设备收到了一个这样的崩溃报告,直接查看 Exception Codes 字段寻找更多的信息。

  • 异常代码 0xbaaaaaad 表明记录是整个系统的 stackshot ,不是一个崩溃报告。为了获得一个 stackshot ,按 Home 键和任意音量键。这些记录经常被用户偶然创建,并不表明是一个错误。

  • 异常代码 0xbad22222 表明一个 VoIP 应用已经被 iOS 终结,因为它启动得太频繁。

  • 异常代码 0x8badf00d 表明应用已经被 iOS 终结因为发生 watchdog 超时。应用花费太长时间启动,终结,或者响应系统事件。通常导致这歌问题是做了在主线程执行了同步的网络请求。无论什么操作在 Thread 0 都需要移动到后台线程,或者异步处理,以免它阻塞主线程。

  • 异常代码 0xc00010ff 表明引用被操作系统终结为了响应一个发热事件。这个可能由于一个发生崩溃的特定的设备的问题或者环境被操作导致。为了使你的应用更高效运行的建议,查看WWDC session iOS Performance and Power Optimization with Instruments

  • 异常代码 0xdead10cc 表明应用被 iOS 终结,由于当在后台运行时它持有了一个系统的资源(像通信录数据库)。

  • 异常代码 0xdeadfa11 表明应用被用户强制退出。强制退出发生在当用户第一次按下开关机按钮直到”滑动来关机”出现,然后在按下 Home 键。这是合理的假如用户这样做了,因为应用已经变得不可响应,但是这并不能保证 - 强制退出任何正在运行的任务。

    注意:终结一个挂起的 app 通过从多任务关系面板中移除并不会产生一个崩溃报告。一旦一个 app 被挂起,iOS它有资格在任何时候终结它,所有没有崩溃报告产生。

参考资料