正文
在iOS开发中,Crash分析一直是稳定攻坚的重头戏。说白了,应用崩溃的根源就是收到了未处理的信号。这些信号来源主要有三个:系统内核、其他进程,以及应用本身。由此,我们也把异常分成了三大类。
- Mach异常:这是底层内核级的异常。对于用户态的开发者,可以通过Mach API设置线程、任务或主机的异常端口来直接捕获这类异常。
- Unix信号(BSD信号):如果Mach异常没有被开发者捕获,系统会在host层通过
ux_exception()将其转换成对应的UNIX信号,再经由threadsignal()投递到出错的线程。我们可以调用signal(x, SignalHandler)来拦截它。 - NSException:应用级的异常,通常指那些没有被捕获的Objective-C异常,会导致程序给自己发送
SIGABRT信号而崩溃。这类异常可以通过try-catch或者NSSetUncaughtExceptionHandler()机制来捕获。
异常
Exception Type:
异常的type基本上固定是SIGABRT。这其实是CrashReporter捕获到异常之后,主动调用abort()发出的信号。正是因为这种机制,如果是Exception导致的崩溃,关键的分析信息在Last Exception Backtrace:这里,而Crash Thread里固定的只是handleException的堆栈,基本没有查看的价值。
Exception Codes:
常见的格式是 0 at 0x18ac2378,后面的地址就是发生异常的对象所处的内存地址。
特殊的 Exception Code
- 0xdead10cc - Deaklock(死锁)
这个崩溃码意味着我们在应用挂起之前,还持有着文件锁或者SQLite数据库锁。正确的做法是在挂起之前主动释放掉这些锁。
- 0xbaaaaaad - Bad
当用户同时按下侧边按钮和两个音量按钮,对整个系统进行stackshot时会触发这个异常码。
- 0xbad22222 - Bad too (two) many times
这很可能是VOIP应用被频繁唤起导致的。另一个需要留意的点是后台网络调用,如果TCP连接被唤醒的次数太多(比如300秒内被唤醒15次),同样会触发此类崩溃。
- 0x8badf00d - Ate (eight) bad food
应用程序在执行状态变更(如启动、关闭、处理系统消息等)时耗时太长,与系统的看门狗计时策略冲突(超时)而被终止。最常见的原因就是在主线程上进行同步网络请求。
- 0xc00010ff - Cool Off
系统检测到设备发烫,主动终止了我们的App。如果只在个别设备上出现,那大概率是硬件问题,而非App自身的bug。但如果涉及设备范围较广,就需要用Instruments去排查App的耗电量了。
- 0x2bad45ec - Too bad for security
触发了安全冲突。如果Termination Description显示为“Process detected doing insecure drawing while in secure mode”,说明应用在无权限的情况下尝试绘制内容,比如在锁屏状态下。
Triggered by Thread:
即发生Crash的线程。
Application Specific Infomation:
这里包含异常的具体信息,是定位问题的关键所在。
Last Exception Backtrace:
这是抛出异常时的代码堆栈。如果是异常崩溃,分析这个堆栈就行了。
主要信号
常见的系统信号包括:SIGTERM、SIGABRT、SIGSEGV、SIGBUS、SIGILL、SIGFPT、SIGKILL、SIGTRAP。
程序结束信号(SIGTERM)和SIGKILL的区别在于它可被阻塞和处理,常用来要求程序主动退出。但在iOS环境下,一般不太会直接处理到它。
SIGABRT原因
- 重复释放(double free)指针。比如分配内存后连续调用两次
free()。 - 释放未初始化或错误的地址。例如直接释放一个野地址。
- 内存越界。比如将一个超长字符串
strcpy到一个长度不够的数组里。 - 直接调用
abort()或assert()。
典型场景
全局变量的赋值代码段被多线程同时调用。上一次赋的值可能在多个线程中同时被释放,从而引发Crash。
解决方案
思路很直接:要么去掉这类赋值操作,要么老老实实加锁。
SIGSEGV原因
- 无效的内存访问(段错误)。
- 野指针访问,访问了没有权限的内存地址或系统内存地址。
- 非ARC模式下,经常出现Delegate对象的野指针访问。
- ARC模式下,Block代码块内强持有可能已被释放的对象。
典型场景
SDWebImageDownloaderOperation的生命周期管理和错误回调分布在两个不同的队列中。有可能self已经进入释放流程,此时再去访问self.completeBlock,就变成了访问无效内存。
根本原因
多线程访问或操作对象、栈溢出。
SIGBUS原因
mmap内存映射的访问超出了大小。- 访问了未对齐的内存地址。
char *p, tmp; NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"txt"]; int fd = open(path.UTF8String, O_RDWR); p = (char*)mmap(NULL,FILESIZE, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0); signal(SIGBUS, handle_sigbus); getchar(); for(int i=0;i
int pi = (int)(0x00001111); *pi = 17;典型场景
用
mmap映射文件内存后,写入时越界。对比理解
SIGSEGV是访问了无效内存——这块内存不属于我们进程,或者我们没有权限;而SIGBUS则是CPU无法操作该地址,大部分情况是因为地址没有对齐。SIGILL原因
- 执行了非法指令。
- 堆栈溢出。
典型场景
在iOS上,这个问题有可能随机出现在任何动态库或静态库的方法中,且一旦出现,应用会持续崩溃。
排查思路
App级别的代码没有修改可执行段的权限,一般无法污染代码段。这类情况通常被归因为苹果底的增量问题,重启手机往往可以解决。
SIGKILL原因
程序接收中止信号,不可被捕获或忽略。OOM、Watchdog最终都会映射为这个异常信号。常见场景包括:
- 长时间占用过多CPU资源被系统杀掉。
- 应用启动时在主线程做长时间操作或卡死,被Watchdog杀死。
- 线程切换过于频繁。
- 占用过多内存被Jetsam杀死。
SIGTRAP原因
系统库如WebKit、libdispatch等常用
__builtin_trap()来触发断点异常。在Debug模式下,这会触发调试器断点,方便实时查看问题;Release模式下,应用直接崩溃,产生SIGTRAP信号。典型场景
dispatch_group_enter和dispatch_group_lea ve调用不匹配。比如dispatch_group_lea ve多调用了一次,会触发DISPATCH_CLIENT_CRASH,内部调用__builtin_trap()制造调试陷阱。Mach 异常
Mach异常的好处在于能捕获更多类型的Crash,比如循环递归导致的堆栈溢出。传统的信号捕获方式会在崩溃线程里执行回调,但堆栈溢出后根本没有环境执行捕获逻辑。而Mach异常捕获可以定义单独的线程来处理异常逻辑。
怎么区分看到的日志是不是Mach异常?很简单:如果
Exception Type:是以EXC_打头的,那就是Mach异常。后面的Exception Subtype:通常是根据Exception Type:转译过来的。Exception Type:
- EXC_BAD_ACCESS:内存不能访问,对应
SIGBUS和SIGSEGV。- EXC_BAD_INSTRUCTION:非法指令,对应
SIGILL。- EXC_ARITHMETIC:算术运算错误,对应
SIGFPE。- EXC_EMULATION:对应
SIGEMT。- EXC_SOFTWARE:软件错误,对应
SIGSYS、SIGPIPE、SIGABRT、SIGKILL。- EXC_BREAKPOINT:对应
SIGTRAP。- EXC_SYSCALL、EXC_MACH_SYSCALL、EXC_RPC_ALERT:不常用。
- EXC_CRASH:对应
SIGBART。- EXC_GUARD:一般是文件句柄防护,比如
close了一个内核的fd。- EXC_RESOURCE:遇到了一些系统资源限制,通常是CPU过载或线程调度太频繁,比如iOS中子线程每秒唤醒次数不能超过150。
Abort
Abort 包含哪些场景?
- 内存使用量过高、短时间内申请大量内存,系统发送信号9(无法通过信号捕获)强制杀死进程(即常说的Jetsam事件)。
- 主线程发生卡死超过一定时间,被Watchdog强制杀死(不同系统版本超时时间不同)。
- 启动超时、后台切前台resume超时。
- 部分死循环、递归等造成的栈溢出。
Abort目标
- 完整捕获现场及上下文信息。
- 精准定位Abort的业务场景和发生原因。
- 基于捕获的现场数据和原因,构建高效的线上分析工具链,快速定位线上崩溃率的主要成因。
Abort推导规则
需要根据可能导致客户端崩溃的原因设计推导规则,并基于线上用户的Abort数据快速聚合,这样才能及时发现并解决影响线上稳定性的大问题。
