App常见崩溃问题分析
前言
此文是基于这些年工作中项目里面常见崩溃的一些总结,整理出来方便查阅,希望对大家都有所帮助。
App常见崩溃
- 数组下标越界
- 字典构造与修改
-
NSAttributedString
相关 - 呈现一个空控制器
- 强引用一个单例对象
- unrecognized selector
- 操作
tableView
数据 - Push到同一个控制器多次
1.数组下标越界
示例代码:
- (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.字典构造造与修改
示例代码:
- (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
的方式来设置字典的值
- (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"];
}
3.NSAttributedString相关
示例代码:
- (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.强引用一个单例对象
异常现象:
App随时有可能面临崩溃,这个在曾经的一次网络请求封装的过程中遇到过,NSURLSession
,不要强引用该对象,否则当你释放引用它的对象然后创建新的对象引用它很可能导致App崩溃。
预防方案:
对单例的Property
不要使用strong
,非要引用的话使用week
。
5.unrecognized selector
示例代码:
- (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数据
示例代码:
- (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)
- 访问一块坏内存
[EXC_BAD_ACCESS] // SIGSEGV // SIGBUS]
- 异常退出
[EXC_CRASH // SIGABRT]
- 追踪受限
[EXC_BREAKPOINT // SIGTRAP]
- 非法指令
[EXC_BAD_INSTRUCTION // SIGILL]
- 被保护的资源遭到侵害
[EXC_GUARD]
- 资源限制
[EXC_RESOURCE]
- 其他异常类型
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 Subtype
和Exception 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它有资格在任何时候终结它,所有没有崩溃报告产生。