Feb 23

OC开发中的异常(Exception)总结

Lrdcq , 2017/02/23 18:29 , 程序 , 閱讀(6888) , Via 本站原創
相对于java从设计之初就养成的一条exception往下流,trycatch到底的作风,在我们iOS开发过程中,oc的异常处理就是一个不可逾越的障碍阻碍着程序的运行与调试。因为oc一般用NSError甩错误,一旦遇到异常,八成就是非常非常严重的不可挽回的错误了,并且由于oc往下直通c层,里面发生的异常简直是多种多样非常难以准确定位和分析。因此,我们来总结一下常见的异常和抓取处理分析方式。

OC Exception

oc层的异常是ios开发中最最最好抓取和分析的异常了。制造一个典型的oc异常简直再简单不过:
NSString *str = nil;
NSDictionary *dic = @{@"key":str};
//or
NSArray *array= @[@"a",@"b",@"c"];
[array objectAtIndex:5];
//or
NSAssert(false, @"OC Exception");

显然,分别是NSDictionary的value不能为空,和NSArray取数据越界,和最暴力的assert直接抛出来的异常。这些在oc层面由iOS库或者各种第三方库或者oc runtime验证出错误而抛出的异常,就是oc异常了。在debug环境下,oc异常导致的崩溃log中都会输出完整的异常信息,比如:*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'OC Exception'。包括这个Exception的类名和描述,下面是这个异常的完整堆栈。所以就算xcode的断点停在了main.m里面,我们也可以轻易的找到异常的位置修复问题。

另外,oc异常还有一个非常好用的特性是可以用trycatch抓住(虽然苹果并不建议这么使用)。例如:
@try {
    NSAssert(false, @"OC Exception");
} @catch (NSException *exception) {
    NSLog(@"%@",exception);
}

就可以获取到当前抛出异常并且阻止异常继续往外抛导致程序崩溃。虽然苹果真的不建议这样做。对于程序真的往外抛出并且我们很难catch到的异常,比如界面和第三方库中甩出来的异常,我们也有方式可以截获到。NSException.m这个文件中携带了一个void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);的函数可以注册一个函数来处理未被捕获的异常。虽然无法阻止程序崩溃,但是可以取得异常进行一些准备和后续处理,使用起来这样:
void HandleException(NSException *exception) {
    NSArray *stackArray = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
    NSLog(@"%@", exceptionInfo);
}

NSSetUncaughtExceptionHandler(&HandleException);

往往我们要做的,是把异常信息保存到本地,等到下次启动的时候进行一些后续处理。这些就是crash收集工具所做的事儿。当然,如果妄想在HandleException时拉界面的话,就算了吧,这个函数运行完成后马上就崩溃了。
另外,这里的堆栈信息包含的是oc方法+指令offset,需要一点方法来对应到具体行数。

Mach Exception

从OC异常往底层走,我们看到的是Mach异常。Mach异常是FreeBSD上特有定义的高层异常,当然,现在网络上能收集到的资料都和mac和ios开发有关。相关的源码网络上可以找到(https://github.com/st3fan/osx-10.9/blob/master/xnu-2422.1.72/osfmk/mach/exception_types.h)。看到异常定义的名称我们会感觉到异常的亲切——EXC_MASK_开头的异常呢。我们一一来总结常见的两个个Mach异常吧:

EXC_BAD_ACCESS (Bad Memory Access)

这是最常见并且我们觉得最头疼的,内存访问错误。这种异常分为两种:
1. 访问对象未初始化(SIGBUS信号)
2. 访问了什么东西已经被回收掉了(SIGSEGV信号)
当然,事实上到底是怎样的错误比上面描述的复杂神秘得多,这才是这个最难处理的主要原因。
EXC_BAD_ACCESS同时也提供了辅助的异常code来帮助我们判断到底是什么错误,比如KERN_PROTECTION_FAILURE是指的地址无权限访问,KERN_INVALID_ADDRESS是指的地址不可用,异常信息中还会包括具体出错的地址。也许可以获得更多的帮助呢。在debug运行是打开内存管理的Zombie Objects可以获得有效的调试信息。

EXC_BAD_INSTRUCTION (Illegal Instruction)

通常通过SIGILL信号触发的异常,很明显,它是在说运行了一条非法的指令。往往错误是这样子的:
XC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
虽然是这样说,都是编译器编译出来的指令怎么会有非法指令嘛。所以事实上遇到这样的问题往往是运行指令的参数不对,多半是为0即nil了。然后我们又回到了空指针的问题了~。
当然,除了代码中的问题。更多的是ios开发中的玄学问题导致的ios本身异常和bug,比如(http://stackoverflow.com/questions/24337791/exc-bad-instruction-code-exc-i386-invop-subcode-0x0-on-dispatch-semaphore-dis)就是这样。解决这些问题,还是得老老实实的分析堆栈猜测和分析了。

其他

其他在实际开发中有可能遇到的并不多,主要是:
1. EXC_RESOURCE是指的程序到达资源上限,比如cpu占用过高,内存不足之类的。这样的问题也没法解决啦。
2. EXC_GUARD是一些c层函数访问错误导致的异常,比如fopen文件访问错误之类的都会爆出这个。不过我们好好的oc不用肯定一般也不会使用这些,所以还安好。
3. 0x00000020,这些是被FreeBSD定义为玄学异常的异常都在里面了,也提供了特殊的code来提供辅助信息。其中其实最常见的code是0x8badf00d,是主线程阻塞时间太长,程序被os杀了。其他的遇到了就是见鬼了!

Unix Signal Exceptions

从Mach异常再往上走追根究底,其实,所以异常发生的本质途径都是Unix的异常信号。
1. OC异常并不是真正的异常,但是当一个OC异常被抛出到最外层还没被谁捕获,程序会强行发送SIGABRT信号中断程序。
2. Mach异常没有比较方便的捕获方式,不过由于它本质就是信号,所以这一段讲的东西也能包含处理Mach异常。

产生一个不属于Mach异常的异常信号也是非常非常简单的事儿,比如:
int *i;
free(i);

总之,c层面,runtime或者其他东西控制程序就是通过信号,中断当然也不例外。通过不同的信号,我们也能知道很多不同的东西。在ios开发环境中,信号枚举在sys/signal.h文件中,我们可以看到大量的Unix信号罗列其中,参考wiki(https://en.wikipedia.org/wiki/Unix_signal)可以看到各个信号的详解。当然,我们最终关心的是能否捕获这些异常信号来抓住异常和崩溃。对,方法是有的,这里提供了一个叫void (*signal(int, void (*)(int)))(int);的方法来注册一个处理函数。

这个方法最后吐出来的是当前的信号,没异常信息堆栈怎么办,还好,从execinfo.h中,我们可以取出当然汇编层程序的堆栈情况。这就好办了,最后处理代码如下:
void SignalExceptionHandler(int signal) {
    NSMutableString *mstr = [[NSMutableString alloc] init];
    [mstr appendString:@"Stack:\n"];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    for (i = 0; i <frames; ++i) {
        [mstr appendFormat:@"%s\n", strs[i]];
    }
}

void InstallSignalHandler(void) {
    signal(SIGHUP, SignalExceptionHandler);
    signal(SIGINT, SignalExceptionHandler);
    signal(SIGQUIT, SignalExceptionHandler);
    signal(SIGABRT, SignalExceptionHandler);
    signal(SIGILL, SignalExceptionHandler);
    signal(SIGSEGV, SignalExceptionHandler);
    signal(SIGFPE, SignalExceptionHandler);
    signal(SIGBUS, SignalExceptionHandler);
    signal(SIGPIPE, SignalExceptionHandler);
}

要注意的是这里获得的堆栈信息是,当前汇编子程序的offset+指令offset,要么我们需要符号表,要么我们需要反编译一些我们的程序来对应代码了。

以上就可以兜底式的涵盖所有的ios应用异常和崩溃的情况。在crash捕获和处理,用户体验优化上可以参考和使用。下一篇博客会继续讲解怎么解读和分析错误堆栈。
关键词:oc , 异常处理 , 崩溃
logo