iOS Crash不崩溃
用户在使用App的过程中,经常遇到闪退的情况,体验不太好,SDK开发公司应更注重这个问题。试想,如果给客户提供一个SDK,用户在使用过程Carskh了,想必开发者是相当抓狂的事情。那么,本文就尝试探索引发闪退的原因,以及在遇到crash的情况下,尽可能的保持程序运行,并及时上报错误。
一、crash类型
## 1.OC层面的crash可能存在的类型
### 1.1 一般carsh类型
NSInvalidArgumentException:非法参数异常,传入非法参数导致异常,nil参数比较常见。
NSRangeException:下标越界导致的异常。
NSGenericException: foreach的循环当中修改元素导致的异常。
1.2 KVO Carsh类型
KVO Crash常见原因:
移除未注册的观察者
重复移除观察者
添加了观察者但是没有实现-observeValueForKeyPath:ofObject:change:context:方法
添加移除keypath=nil
添加移除observer=nil
1.3 unrecognized selector sent to instance Carsh类型
对象接收到未知的消息,即下图中消息未能处理的情况。
2.Signal层面的crash
除了OC层面的异常捕获之外,很多内存错误、访问错误的地址产生的crash则需要利用unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。该函数中我们可以输出栈信息,版本信息等其他一切我们所想要的。
SIGKILL:用来立即结束程序的运行的信号。
SIGSEGV:试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
SIGABRT:调用abort函数生成的信号。
SIGTRAP:由断点指令或其它trap指令产生。
SIGBUS:非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
二、存在问题
程序闪退,用户体验不好
三、监听crash
1.任凭程序闪退并上报
1.1 NSSetUncaughtExceptionHandler 捕获OC层面的crash
参考文章
(1)App工程中的AppDelegate中添加NSSetUncaughtExceptionHandler()函数可以捕获到堆栈异常。
-(BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions{
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
returnYES;
}
voidUncaughtExceptionHandler(NSException*exception) {
/**
* 获取异常崩溃信息
*/
NSArray*callStack=[exceptioncallStackSymbols];
NSString*reason=[exceptionreason];
NSString*name=[exceptionname];
/// TODO: 上报异常信息
}
voidInstallSignalHandler(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);
}
voidSignalExceptionHandler(intsignal)
{
NSMutableString*mstr=[[NSMutableStringalloc]init];
[mstrappendString:@"Stack:\n"];
void*callstack[128];
inti,frames=backtrace(callstack,128);
char**strs=backtrace_symbols(callstack,frames);
for(i=0;i [mstrappendFormat:@"%s\n",strs[i]]; } [SignalHandlersaveCreash:mstr]; } [selfexchangeInstanceMethod:__NSArrayImethod1Sel:@selector(objectAtIndex:)method2Sel:@selector(avoidCrashObjectAtIndex:)]; -(id)avoidCrashObjectAtIndex:(NSUInteger)index{ idobject=nil; @try{ object=[selfavoidCrashObjectAtIndex:index]; } @catch(NSException*exception) { //捕获异常,根据exception打印出堆栈信息,同时也避免了程序崩溃 } @finally{ returnobject; } } /// 注意:使用方法进行捕获异常之后,第三方工具将不会搜集到崩溃信息并上报,需要在catch中手动上报。 注意:使用方法进行捕获异常之后,第三方工具将不会搜集到崩溃信息并上报,需要在catch中手动上报。 if([selfrespondsToSelector:@selector(method)]) { [selfperformSelector:@selector(method)]; } -(void)forwardInvocation:(NSInvocation*)anInvocation -(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector [selfexchangeInstanceMethod:[selfclass]method1Sel:@selector(methodSignatureForSelector:)method2Sel:@selector(avoidCrashMethodSignatureForSelector:)]; [selfexchangeInstanceMethod:[selfclass]method1Sel:@selector(forwardInvocation:)method2Sel:@selector(avoidCrashForwardInvocation:)]; #pragma mark - -(NSMethodSignature*)avoidCrashMethodSignatureForSelector:(SEL)aSelector { NSMethodSignature*ms=[selfavoidCrashMethodSignatureForSelector:aSelector]; if([selfrespondsToSelector:aSelector]||ms){ returnms; } else{ return[SafeProxyinstanceMethodSignatureForSelector:@selector(safe_crashLog)]; } } #pragma mark - -(void)avoidCrashForwardInvocation:(NSInvocation*)anInvocation{ @try{ [selfavoidCrashForwardInvocation:anInvocation]; }@catch(NSException*exception) { //捕获异常,根据exception打印出堆栈信息,同时也避免了程序崩溃 //上报 }@finally{ } } 参考文章(Objective-C Runtime 运行时之三:方法与消息) 注: objc_destructInstance会释放与实例相关联的引用,但是并不释放该实例的内存。 4.及时释放zombieObject。 3.Swizzle消息转发机制forwardingTargetForSelector方法,处理所 有原始类originObject的方法,收集错误信息并上报。 2.Swizzle原有dealloc方法,如果有野指针防护标记,调用 objc_destructInstance方法,修改实例isa使其指向zombieObject,保存原始 类名,以便上报使用。 1.Swizzle原有allocWithZone方法,添加野指针防护标记。 模仿Xcode的zombie机制: 2.4 针对野指针的处理机制 注意:使用方法进行捕获异常之后,第三方工具将不会搜集到崩溃信息并上报,需要在catch中手动上报。 打印出了堆栈信息,同时避免了程序崩溃。 NSInvalidArgumentException *** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1] Error Place:-[ViewController NSArray_Test_InstanceArray] AvoidCrash default is to remove nil object and instance a array. 效果如下: [self exchangeInstanceMethod:[self class] method1Sel:@selector(doesNotRecognizeSelector:) method2Sel:@selector(avoidCrashDoesNotRecognizeSelector:)]; #pragma mark - - (void)avoidCrashDoesNotRecognizeSelector:(SEL)aSelector{ @try { [self avoidCrashDoesNotRecognizeSelector:aSelector]; } @catch (NSException *exception) { //捕获异常,根据exception打印出堆栈信息,同时也避免了程序崩溃 //上报 } @finally { } } 方法二:直接hook doesNotRecognizeSelector也可实现,doesNotRecognizeSelector起到抛出异常的作用,自己增加try-catch进行捕获即可,代码如下: 方法一:hook上述两个方法,在methodSignatureForSelector中返回有效的NSMethodSignature,在forwardInvocation中添加try-catch即可,代码如下: 如果都不中,调用doesNotRecognizeSelector抛出异常。 3、调用methodSignatureForSelector(函数符号制造器)和forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。 2、调用forwardingTargetForSelector让别的对象去执行这个函数 1、调用resolveInstanceMethod给个机会让类添加这个实现这个函数 上图可以看出,在一个函数找不到时,Objective-C提供了三种方式去补救: 当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃。 通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示: 2.3 针对unrecognized selector解决方案 移除未注册的观察者:在移除A对象的观察者时,先判断数组中是否有A对象的观察者,如果有,再移除。 重复移除观察者:同上 添加了观察者但是没有实现-observeValueForKeyPath:ofObject:change:context:方法:hook observeValueForKeyPath方法,增加try-catch即可。 添加移除keypath=nil:hook添加移除观察者的方法,在新方法中过滤keypath=nil的情况。 添加移除observer=nil:hook添加移除观察者的方法,在新方法中过滤observer=nil的情况。 新建一个对象,用来记录target,observer,context,keypath等,每添加一个监听,增加一个对象,用一个数组维护。添加和删除的时候做判断,同时hook dealloc函数,dealloc的同时移除我的观察者和我观察的对象。dealloc时遍历数组,数组中不应该存在对象,如果存在对象,应该抛出异常并接收,提示用户KVO的释放存在问题。 2.2 针对KVO Crash的处理机制 hook相关的方法,增加保护机制。 以NSArray越界为例,hook objectAtIndex方法,在方法中捕获越界异常,并在最后返回一个nil对象。 2.1 针对一般类型Crash的处理机制 2.Crash自动修复+捕获上报 1.2 在Appdelegate中可以注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数,处理Signal层面的crash。 (2)通过监听捕获,解析堆栈信息并上报