应用程序崩溃是程序开发过程中除了bug外一直伴随我们的最大的幽灵,时不时给我们致命一击。
应用程序崩溃的原因有很多,一般应用程序在崩溃时会触发相应的异常退出信号。iOS应用程序崩溃信号可以分为操作系统异常信号,iOS系统限制以及语言运行时错误。
操作系统常见异常
由于iOS源自MacOS,而MacOS由基于Unix并被Apple添加了很多自定义组件。在iOS系统中会出现两种异常
Mach异常与UNIX异常
- Mach异常
Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常。每个thread,task,host都有一个异常端口数组,Mach的部分API暴露给了用户态,用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常,抓取Crash事件。
所有Mach异常都在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。iOS中的 POSIX API就是通过Mach之上的 BSD层实现的。
Mach微内核中有几个基础概念:
Tasks,拥有一组系统资源的对象,允许"thread"在其中执行。
Threads,执行的基本单位,拥有task的上下文,并共享其资源。
Ports,task之间通讯的一组受保护的消息队列;task可对任何port发送/接收数据。
Host, Mach 最基础的对象是“主机(host)”,也就是表示机器本身的对象。主机对象是一个简单的数据结构。主机只不过是一组“特殊端口”的集合(用于向主机发送各种消息),以及一组异常处理程序的集合。主机定义了一个锁组用于保护异常处理的并发访问。主机的数据结构主要有三个基本功能:
提供机器信息:Mach 提供了一组异常丰富的API调用用于查询机器信息,所有这些调用都要求获得主机端口才能工作。
提供子系统的访问:通过主机抽象,应用程序可以请求访问子系统使用的任何“特殊”端口。此外,还可以获得所有其他机器抽象(例如:processor 和 processor_set)的访问权。
提供默认的异常处理:异常从线程基本提升到进程(任务)基本,如果没有被处理的话。则进一步提升到主机级别做通用的处理。
- UNIX信号
这就是我们常说的信号了,实际上由于Mach异常会被转换为Unix信号,通常我们看到的crash都是触发为unix异常信号。常见的Unix信号如下:
信号 | 说明 |
---|---|
SIGHUP | 用户终端退出进程时,终端将接收到SIGHUP信号。这个信号的默认操作为终止进程。iOS中通常不出现该信号 |
SIGTERM | 终止请求,发送到程序(软kill),iOS中通常不出现该信号 |
SIGSEGV | 无效的内存访问(分段故障) |
SIGINT | 外部中断,通常由用户发起,程序终止信号(interrupt),在用户键入INTR字符(通常Ctrl-C)时发出,用于通知前台进程组终止进程。iOS中通常不出现该信号 |
SIGILL | 无效的程序映像,如无效指令,通常是kill命令调用产生(强制kill),iOS下杀死app会走应用程序周期,一般不出现该信号 |
SIGABRT | 异常终止条件,例如由中止abort函数调用 |
SIGFPE | 错误的算术运算,如除以零 |
SIGBUS | 试图访问一块无文件内容对应的内存区域,比如超过文件尾的内存区域,或者以前有文件内容对应,现在为另一进程截断过的内存区域。 |
SIGTRAP | 常来说SIGTRAP是由断点指令或其它trap指令产生. 由debugger使用。如果没有附加调试器,则该过程将终止并生成崩溃报告。 较低级的库(例如,libdispatch)会在遇到致命错误时捕获进程。 |
SIGABRT
在C/C++的库中有较多触发SIGABRT的场景
- 多次free指针
#include "stdlib.h"
#include "string.h"
#include "stdio.h
int main()
{
void *pc = malloc(1024);
free(pc);
//free(pc); //打开注释会导致错误
printf("free ok!\n");
return 0;
}
- C/C++中使用abort函数
#include "stdlib.h"
int main()
{
printf("before run abort!\n");
abort();
printf("after run abort!\n");
return 0;
}
assert函数内部也是会调用abort。在使用一些系统库容易出现SIGABRT异常,在一些被禁止调用的函数被调用时也会出现该异常错误。
- 在C/C++中对容器类的越界访问也会产生该信号Crash
(备注:OC中容器访问越界会触发SIGSEGV信号,或NSRangeException异常)
int i = 0;
int arr[3] = {0}; // 包含三个 int 元素的数组,并且,每个元素的值初始化为 0
for (; i < 4; i++) { // i < 4, 这个地方会造成数组越界
arr[i] = i;
cout << "arr[" << i << "] = " << arr[i] << endl;
}
SIGSEGV
SIGSEGV错误是我们应用最常见的错误,SIGSEGV在很多时候是由于指针越界引起的,但并不是所有的指针越界都会引发SIGSEGV。一个越界的指针,如果不解引用它,是不会引起SIGSEGV的。而即使解引用了一个越界的指针,也不一定会引起SIGSEGV。
非法的内存访问
错误的访问类型
#include <stdio.h>
#include <stdlib.h>
int main() {
char* s = "hello world";
s[1] = 'H';
}
这是最常见的一个例子。此例中,”hello world”作为一个常量字符串,在编译后会被放在.rodata节(GCC),最后链接生成目标程序时.rodata节会被合并到text segment与代码段放在一起,故其所处内存区域是只读的。
这就是错误的访问类型引起的SIGSEGV。
- 访问非进程空间内存
内存地址不在进程的地址空间之内
以空指针为代表的程序起始空间
未申请的堆空间
段与段之间的空洞
内存地址空间合法,但是权限不满足
对代码段进行写操作:野指针,向代码段进行写操作
对数据段进行执行操作:rip错误,把数据段的数据当作指令来执行
访问不存在内存:invalid memory access (segmentation fault),例如
char *p = NULL;
*p = 1;
多线程访问同一内存地址
函数跳转到了一个非法的地址上执行
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void foo () {
char c;
memset (&c, 0x55, 128);
}
int main () {
foo();
}
通过栈溢出,我们将函数foo的返回地址覆盖成了0x55555555,函数跳转到了一个非法地址执行,最终引发SIGSEGV。
SIGBUS
一般是由于地址未对齐导致的,例如内存地址对齐出错,或者试图执行没有权限的代码地址。子码有以下几种情况:
KERN_MEMORY_ERROR:试图访问当时无法返回数据的内存,如内存映射文件不可用。
EXC_ARM_DA_ALIGN:试图访问没有正确对齐的内存。此异常代码很少见,因为64位ARM CPU可处理未对齐的数据。但是,如果内存地址既未对齐又位于未映射的内存区域中,则可能会看到此异常子类型。
SIGILL
常见EXC_BAD_INSTRUCTION非法指令,通常与特定非法或未定义指令或操作数相关。
EXC_BREAKPOINT(SIGTRAP)
在ARM处理器上,断点异常类型指示跟踪陷阱中断进程。 跟踪陷阱使附上的调试器有机会在执行特定位置时中断该进程。
断点异常类型指示跟踪陷阱中断了该过程。 跟踪陷阱使附加的调试器有机会在执行的特定点中断该进程。 在ARM处理器上,它显示为EXC_BREAKPOINT(SIGTRAP)。 在x86_64处理器上,它显示为EXC_BAD_INSTRUCTION(SIGILL)。
Swift运行时将跟踪陷阱用于特定类型的不可恢复的错误-有关这些错误的信息,请参见Addressing Crashes from Swift Runtime Errors。 一些较低级别的库(例如Dispatch)会在遇到不可恢复的错误时使用此异常来捕获进程,并在崩溃报告的“其他诊断信息”部分中记录有关该错误的其他信息。 有关这些消息的信息,请参阅Diagnostic Messages。
当使用swift时,以下几种情况也会抛出此异常:
一个非可选类型值为nil;
强制类型转换失败;
如果要在自己的代码中使用相同的技术来解决不可恢复的错误,请调用__builtin_trap()函数。 这使系统可以生成带有线程回溯的崩溃报告,以显示你如何达到不可恢复的错误。
- ILL_ILLTRP:ILL_ILLTRP at 0xxxxx通常是二进制出错,典型比如app升级前后二进制缓存出错。
SIGFPE
崩溃的线程执行了无效的算术运算。
包括除以0或取余0的情况,及发生数据溢出导致的除以0或取余0的情况;包括浮点错误。
The following values can be placed in si_code for a SIGFPE signal:
FPE_INTDIV Integer divide by zero.
FPE_INTOVF Integer overflow.
FPE_FLTDIV Floating-point divide by zero.
FPE_FLTOVF Floating-point overflow.
FPE_FLTUND Floating-point underflow.
FPE_FLTRES Floating-point inexact result.
FPE_FLTINV Floating-point invalid operation.
FPE_FLTSUB Subscript out of range.
SIGKILL
此信号表示系统中止进程,通常是调用函数exit()或kill(9)产生。iOS中常见为受到系统资源限制而导致退出
崩溃报告会包含代表中止原因的编码:
0x8badf00d:eate bad food,系统监视程序由中止无响应应用。注意在生命周期的不同阶段,触发看门狗机制的超时时间是不一样的。
0xc00010ff:cool off,系统由于过热保护中止应用,通常与特定的手机和环境有关。
0xdead10cc:dead lock,系统中止在挂起期间一直保持文件锁或SQLite数据库锁的应用。
0xbaadca11:bad all,系统由于应用在响应PushKit通知时无法报告CallKit呼叫而中止它。
0xbad22222:系统由于VoIP应用恢复太频繁而中止它。
0xc51bad01:OS终止了该应用程序,因为它在执行后台任务时占用了过多的CPU时间。
0xc51bad02:OS终止了该应用程序,因为它未能在分配的时间内完成后台任务。
0xc51bad03:OS终止了该应用程序,因为它未能在分配的时间内完成后台任务,但是系统总体上非常繁忙,以至于该应用程序可能没有收到太多的CPU时间来执行后台任务。
0xbada5e47:系统可能由于你启动了过多了后台任务而中止你的应用。
iOS系统异常和限制
由于iOS通常运行在移动设备上,为了保证移动设备的使用体验,iOS操作系统通常会设定各种限制
- UI线程无响应
通常来自主线程阻塞,操作系统为保障应用程序的流畅会对主线程进行watchdog监听,如果发现主线程在较长时间都没有处理UI刷新或者事件响应,会触发watchdog的kill机制。引起主线程无响应的情况有多种可能,常见场景有:
-
主线程死锁
使用dispath_sync不规范导致死锁
在主线程中执行过长任务或者进行死循环
主线程等待信号时间过长。
启动时间过长
启动时间过长通常也是由于主线程无响应导致的,例子:
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d
Termination Description: SPRINGBOARD, scene-update watchdog transgression: application<com.soulapp.cn>:1952
exhausted real (wall clock) time allowance of 10.00 seconds | ProcessVisibility: Foreground | ProcessState: Running | WatchdogEvent: scene-update
| WatchdogVisibility: Foreground | WatchdogCPUStatistics:
( | "Elapsed total CPU time (seconds): 33.200 (user 33.200, system 0.000), 80% CPU", | "Elapsed application CPU time (seconds): 3.620, 9% CPU" | )
Triggered by Thread: 0
- 触发系统资源限制
EXC_GUARD
受保护资源的非法访问,一般是由违背受保护资源防护触发,例如非法访问某些文件描述符。
EXC_RESOURCE
资源受限,应用由于达到资源的消耗限制而被退出,例如:
CPU使用过高
内存使用过高
weakups过高
OC语言异常
OC运行时也有一些自己特点的错误,通常表现为异常退出
异常 | 说明 | 触发原因 |
---|---|---|
NSGenericException | 通用异常 | |
NSRangeException | 访问越界异常 | 访问数组,容器类越界 |
NSInvalidArgumentException | 非法参数异常 | 是 Objective -C 代码最常出现的错误,所以平时在写代码的时候,需要多加注意,加强对参数的检查,避免传入非法参数导致异常,其中尤以nil参数为甚。 |
NSInternalInconsistencyException | 内部不一致异常 | 通常出现为对非mutable容器当成mutable容器使用,也见于把Xib当成Storyboard使用 |
NSMallocException | 分配内存异常 | 通常是内存不足引起的,如一次性申请过大内存空间,图片占有内存过大。iOS系统有时候也会限制在短时间内频繁申请和释放内存行为 |
NSObjectInaccessibleException | 对象不可访问异常 | 常见于使用CoreData中对象被删除或者不可用,也出现于远程过程调用中对象不可访问 |
NSObjectNotAvailableException | 对象不存在异常 | 访问一些被限制的对象/方法,例如:在SwiftUI APP中使用alertView,远程过程调用对象不存在,使用WebKitView可能会遇到 |
NSDestinationInvalidException | 目的不合法异常 | 常见于发起远程连接时地址不合法 |
NSPortTimeoutException | 通信超时异常 | 在建立网络连接通信超时 |
NSInvalidSendPortException | 发送端口已经失效 | NSConnection断开/失效时发送方收到异常 |
NSInvalidReceivePortException | 接收端口失效异常 | NSConnection断开/失效时接收方收到异常 |
NSPortSendException | 端口发送异常 | NSConnection连接使用时可能遇到 |
NSPortReceiveException | 端口接收异常 | NSConnection连接使用时可能遇到 |
NSOldStyleException | 老式异常 | 已经不在使用 |
NSInconsistentArchiveException | 初始化或者编码异常 | 通常出现在文档解析,存储解析操作 |
此外还有一些自定义的异常,比如:
FileNotFoundException 文件读写时找不到异常
参考: