作为一名开发人员,如何定位解决线上Crash是每一个必修的课题。那问题来了,Crash 如何产生?作为一名 iOS 开发,我今天主要分享 iOS Crash 相关的内容
Crash 产生
Crash 产生一般分两种情况:
- 应用程序自己调用退出函数。eg:调用自杀函数 kill()
- 系统把你应用杀死。eg:应用内存占用过高时,系统保护机制会把你的应用干掉。
(各种崩溃场景后续再讨论)
想清楚以上两个场景之后我们我们来看几个元凶。
- 软件异常
软件异常主要来源于两个 API 的调用 kill() 、 pthread_kill() , 而 iOS 中我们常常遇到的 NSException 未捕获、 abort() 函数调用等,都属于这种情况。比如我们常看到 Crash 堆栈中有 pthead_kill 方法的调用。
(此刻我知道你的内心活动,**从来没掉过啊。没错你是没掉,但是系统库会 eg:libc,刺激不刺激,惊喜不惊喜) - 硬件异常
硬件产生的信号始于处理器 trap,处理器 trap 是平台相关的。出现这种情况,计算机会暂停当前程序,及时转入故障处理。比如我们遇到的野指针崩溃大部分是硬件异常。 - Mach异常
这里虽然叫异常,但是要和上面的两种分开来看。我们了解到苹果的内核 xnu 的核心是 Mach , 在 Mach 之上建立了 BSD 层。“Mach异常” 是 “Mach异常处理流程” 的简称。不懂没事,后面有讲解
除了以上三个大杀器,还有一个是程序语言异常,这个在我们的Crash日志也是比较常见的,但是与以上三个不同,这种异常往往是我们代码逻辑不合理造成的。没错,说的就是看文章的你,为啥会崩溃,你心里一点数都没有吗????这种稍后我也会给大家总结
接下来我们看一下 Mac OS & iOS 是如何处理这些异常的。以下内容来自“深入解析 Mac OS X & iOS 操作系统”
软件异常处理:
硬件异常处理:
通过上面两张图,我们可以很清楚的看到,无论是软件异常,还是硬件异常,最终都会被转换为信号,然后通过act_set_astbsd()发送给我们的应用进程,唤醒其中的某个线程响应指定操作(记住这句话,我们后续有大用)。此时的信号是 UNIX 信号,如 SIGBUS SIGSEGV SIGABRT SIGKILL 等。此时大家可以去翻翻后台的Crash日志,你会发现好多这中以SIG开头的Crash。具体信号的含义大家可以自行 Google,或者直接点击文章参考参考查看。
UNIX信号抛出简单流程大致如下:
附 “Mach异常” 与 “UNIX信号” 的转换关系代码,来自 xnu 中的 bsd/uxkern/ux_exception.c :
switch(exception) {
case EXC_BAD_ACCESS:
if (code == KERN_INVALID_ADDRESS)
*ux_signal = SIGSEGV;
else
*ux_signal = SIGBUS;
break;
case EXC_BAD_INSTRUCTION:
*ux_signal = SIGILL;
break;
case EXC_ARITHMETIC:
*ux_signal = SIGFPE;
break;
case EXC_EMULATION:
*ux_signal = SIGEMT;
break;
case EXC_SOFTWARE:
switch (code) {
case EXC_UNIX_BAD_SYSCALL:
*ux_signal = SIGSYS;
break;
case EXC_UNIX_BAD_PIPE:
*ux_signal = SIGPIPE;
break;
case EXC_UNIX_ABORT:
*ux_signal = SIGABRT;
break;
case EXC_SOFT_SIGNAL:
*ux_signal = SIGKILL;
break;
}
break;
case EXC_BREAKPOINT:
*ux_signal = SIGTRAP;
break;
}
看了以上内容,我想你的内心一定是
老实说以上理解以上内容确实需要你有相当的计算机基础功力才行,为了照顾你的心情,接下来咱们聊点你能看懂的。
除了上面咱们说道的,还有一种程序语言异常,这种异常通常是程序语言自己封装好的。产生的原因通常是因为程序员编写逻辑错误造成的,没错,就是你自己造的孽。作为iOS程序员,我们主要是关注的是 Objec-C 和 Swift。
Object-C 的异常的异常主要是NSException对象封装的,比较多,咱们这里看几个比较常见的,文末有参考中有链接大家可以看到所有的异常类型。
先来个王炸,这个你肯定见过:
-
NSInvalidArgumentException
传递非法参数给一个方法时抛出的异常。
常见场景:- NSNutableDictionaryr操作key或value的函数,如setObject:forKey:、removeObjectForKey等等。
- NSMutableArray操作value的函数,如addObject:、 insertObject:atIndex:等等。
- NSString操作函数,如initWithString:、initWithFormat:、stringWithString:等等。
这里要住意多线程操作,这个坑,深到一半人爬不出来,比如我前面的小朋友,直接被搞离职了
-
NSRangeException:
尝试访问某些数据范围之外时抛出的异常。
常见场景:- NSArray包含索引的操作,如insertObject:atIndex:、objectAtIndex:等等。
- NSString包含索引的操作,如characterAtIndex:、getCharacters:range:等等。
-
NSFileHandleOperationException
如果尝试确定文件句柄类型失败或尝试读取文件或通道失败,则会抛出此异常。
常见场景:- 空间不足:会提示No space left on devie。
- 没有读写权限:会提示Bad file descriptor。
- 读文件失败。
在操作文件时,要验证文件句柄的有效性,对文件大小进行校验,对存储空间进行判断。
-
KVO引起的异常:
常见场景:- 多次移除KVO
抛出NSRangeException异常 - 添加或移除时keyPath参数为nil
- 没有实现observeValueForKeyPath方法(非必显, iOS14)
我在当前 iOS 15的系统测试过,无论是没有移除观察者还是VC推出时没有一处观察者都没出现崩溃。多次移除相同Path时和path名字为nil会出现Crash
- 多次移除KVO
-
iOS Crash之NSMallocException
常见场景:- 分配的空间过大
- 图像占用空间过大
- OOM问题。这个主要是程序死循环造成的
-
NSGenericException
常见场景:- 可变对象遍历过程中发生了改变。
以上大概就是我们开发和线上应用常见一些异常。至于swift,应为NSexception是Cocoa框架的,swift也绕不过,所以上面的异常,swift也是都有的,当然由于语言上的设计优势,swift出现的可能行降低一些。swift 该只有 try! 解析空。其他欢迎大家补充。
毕竟是初始 iOS Crash,就给大家分析到这里吧,下一篇我们看一下crash收集和符号化。
参考文档:
处理器陷阱 。
UNIX 信号
iOS Crash 分析策略
野指针
理解异常类型
Object-C Exception
iOS内功篇:浅谈Crash