异常分类
软件异常(OC异常)
主要来源于 kill() 、 pthread_kill() 两个 API 的调用, 而 iOS 中我们常常遇到的 NSException 未捕获、 abort() 函数调用等,都属于这种情况。比如我们常看到 Crash 堆栈中有 pthead_kill 方法的调用。当一个OC异常被抛出到最外层还没被捕获,程序会强行发送SIGABRT信号中断程序。如果使用try catch捕获此异常,应用不会闪退。
kill() 、 pthread_kill()两个方法分别是向进程、线程发送信号,而不是字面意思直接杀死进程or线程,只不过是因为大部分signal都是杀死线程or进程的。
如下面各类未捕获的NSException,常见的unrecognized selector sendt to instance
就属于第一个NSInvalidArgumentException
NSInvalidArgumentException
NSRangeException
NSGenericException
NSInternalInconsistencyException
NSFileHandleOperationException
软件异常 -> UNIX信号
硬件异常(Mach异常)
底层的内核级异常。
例如平常的异常EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach层的EXC_BAD_ACCESS
异常,在BSD层被转换成SIGSEGV
信号发送到出错的线程。
Mach异常最终会经过BSD层会转化成信号,可以通过捕获信号,来捕获 Crash异常 事件。
硬件异常 -> Mach异常 -> UNIX信号
异常捕获
无论是硬件产生的信号,还是软件产生的信号,都会走到 act_set_astbsd() 进而唤醒收到信号的进程的某一个线程。这个机制就给我们在“自身进程内捕获 Crash” 提供了可能性。就是可以通过拦截 “UNIX信号” 或 “Mach异常” 来捕获崩溃。
- 软件异常(OC异常)
NSException异常是比较容易处理的,通过注册NSUncaughtExceptionHandler
捕获异常信息即可
// register the uncaught exception handler
NSSetUncaughtExceptionHandler(&handler);
- 硬件异常(Mach异常)
通过注册signalHandler来捕获信号。再如EXC_CRASH
异常,在BSD层会被转换成SIGABRT
信号发送出去。
Mach异常与Signal信号对应
保持异常发生现场
下面代码是捕获NSException异常
// 捕获Mach异常
NSSetUncaughtExceptionHandler(&handleException);
在handleException
回调函数中,可以获取到当前的RunLoop,然后获取该RunLoop中的所有Mode,手动运行一遍。这样就保持达到拦截Crash,保证App不崩溃。
- (void)handleException:(NSException *)exception
{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (YES) {//强制进入死循环
for (NSString *mode in (__bridge NSArray *)allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
//解除监听异常
NSSetUncaughtExceptionHandler(NULL);
}
野指针
当一个指针所指向的对象被释放或者收回,但是该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,这个指针就是野指针。
发生野指针时,App不一定会立马产生闪退,因为因为类的dealloc
方法执行后只是告诉系统,这片内存不用了,而系统并没有让这片内存不能访问。此时下次访问前,这块内存还没被覆盖,那使用这个指针还可能是正常的。当然如果这块内存被覆盖了,那就产生闪退。
针对这种问题,一般的优化思路是让dealloc之后,强制覆盖这块内存区域。
Xocde有自带的工具如下
‘
1、Malloc Scribble ,其官方解释如下:申请内存 alloc 时在内存上填0xAA,释放内存 dealloc 在内存上填 0x55。
2、Zombie Objects,其官方解释如下:一个对象已经解除了它的引用,已经被释放掉,但是此时仍然是可以接受消息,这个对象就叫做Zombie Objects(僵尸对象)。
僵尸原理大概如下,当dealloc方法执行时,动态生成一个僵尸对象类,并修改当前的对象的isa指针指向新生成的僵尸对象类,这个新的类中只有一个isa指针,里面没有其他的属性和方法,所以不能响应任何事件,所以在向这个僵尸对象发送消息时,就会必须crash。
当然上述两种方法都是要借助Xcode调试才能使用,如果是测试或者线上的环境,就难以实现,特别是线上的crash,野指针发生不一定crash,等发生crash时,堆栈信息参考性不准确。
定位野指针
主要思路是通过Facebook的fishhook库,hook系统的free函数,在释放的时候覆盖为0x55。
1、通过fishhook替换C函数的free方法为自定义的safe_free。
2、在safe_free方法中对已经释放变量的内存,填充0x55,使已经释放变量不能访问,从而使某些野指针的crash从不必现安变成必现。
参考
https://juejin.cn/post/6968700344050122766#heading-5
https://developer.aliyun.com/article/766088