crash捕获及处理

作者: 乳猪啸谷 | 来源:发表于2018-10-25 11:27 被阅读1次

    一、crash类型

    1.OC层面的crash

    1.1 普通类型

    • NSArray 越界
    • NSCache key或value为nil
    • NSDictionary nil

    1.2 KVO

    • 移除未注册的观察者
    • 重复移除观察者
    • 添加了观察者但是没有实现-observeValueForKeyPath:ofObject:change:context:方法
    • 添加移除keypath=nil
    • 添加移除observer=nil

    1.3 unrecognized selector sent to instance

    • 对象接收到未知的消息

    2.Signal层面的crash

    除了OC层面的异常捕获之外,很多内存错误、访问错误的地址产生的crash则需要利用unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。该函数中我们可以输出栈信息,版本信息等其他一切我们所想要的。

    • SIGKILL:用来立即结束程序的运行的信号。
    • SIGSEGV:试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
    • SIGABRT:调用abort函数生成的信号。
    • SIGTRAP:由断点指令或其它trap指令产生。
    • SIGBUS:非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

    二、存在问题

    • 第三方库规范问题
    • 不同开发者的规范问题
    • 存在一些低级的crash

    三、监听crash

    1.RDM 监听机制

    1.1 NSSetUncaughtExceptionHandler 捕获OC层面的crash

    参考文章

    (1)AppDelegate中添加捕获监听

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
      NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
      return YES;
      }
    

    (2)解析堆栈信息并上报

    void UncaughtExceptionHandler(NSException *exception) {
      /**
       *  获取异常崩溃信息
       */
      NSArray *callStack = [exception callStackSymbols];
      NSString *reason = [exception reason];
      NSString *name = [exception name];
    }
    

    1.2 Appdelegate中注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数,处理Signal层面的crash。

    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);
    }
    
    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]];
      }
      [SignalHandler saveCrash:mstr];
    

    2.对OC层面crash的捕获和处理

    2.1 针对普通类型Crash的处理机制

    hook相关的方法,增加保护机制。
    以NSArray越界为例,hook objectAtIndex方法,在方法中捕获越界异常,并在最后返回一个nil对象。

    [self exchangeInstanceMethod:__NSArrayI method1Sel:@selector(objectAtIndex:) method2Sel:@selector(avoidCrashObjectAtIndex:)];
    
    - (id)avoidCrashObjectAtIndex:(NSUInteger)index {
      id object = nil;
      
      @try {
          object = [self avoidCrashObjectAtIndex:index];
      }
      @catch (NSException *exception) {
         //捕获异常,根据exception打印出堆栈信息,同时也避免了程序崩溃
      }
      @finally {
          return object;
      }
    }
    

    注意:使用方法进行捕获异常之后,第三方工具将不会搜集到崩溃信息并上报,需要在catch中手动上报。

    2.2 针对KVO Crash的处理机制

    新建一个对象,用来记录target,observer,context,keypath等,每添加一个监听,增加一个对象,用一个数组维护。添加和删除的时候做判断,同时hook dealloc函数,dealloc的同时移除我的观察者和我观察的对象。dealloc时遍历数组,数组中不应该存在对象,如果存在对象,应该抛出异常并接收,提示用户KVO的释放存在问题。

    • 移除未注册的观察者:在移除A对象的观察者时,先判断数组中是否有A对象的观察者,如果有,再移除。
    • 重复移除观察者:同上
    • 添加了观察者但是没有实现-observeValueForKeyPath:ofObject:change:context:方法:hook observeValueForKeyPath方法,增加try-catch即可。
    • 添加移除keypath=nil:hook添加移除观察者的方法,在新方法中过滤keypath=nil的情况。
    • 添加移除observer=nil:hook添加移除观察者的方法,在新方法中过滤observer=nil的情况。

    注意:使用方法进行捕获异常之后,第三方工具将不会搜集到崩溃信息并上报,需要在catch中手动上报。

    2.3 针对unrecognized selector解决方案

    通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:

    if ([self respondsToSelector:@selector(method)]) {
    [self performSelector:@selector(method)];
    }
    

    当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃。

    image

    上图可以看出,在一个函数找不到时,Objective-C提供了三种方式去补救:

    1、调用resolveInstanceMethod给个机会让类添加这个实现这个函数

    2、调用forwardingTargetForSelector让别的对象去执行这个函数

    3、调用methodSignatureForSelector(函数符号制造器)和forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。

    如果都不中,调用doesNotRecognizeSelector抛出异常。

    - (void)forwardInvocation:(NSInvocation *)anInvocation

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

    方法一:hook上述两个方法,在methodSignatureForSelector中返回有效的NSMethodSignature,在forwardInvocation中添加try-catch即可,代码如下:

     [self exchangeInstanceMethod:[self class] method1Sel:@selector(methodSignatureForSelector:) method2Sel:@selector(avoidCrashMethodSignatureForSelector:)];
     [self exchangeInstanceMethod:[self class] method1Sel:@selector(forwardInvocation:) method2Sel:@selector(avoidCrashForwardInvocation:)];
    
    - (NSMethodSignature *)avoidCrashMethodSignatureForSelector:(SEL)aSelector
    {
        NSMethodSignature *ms = [self avoidCrashMethodSignatureForSelector:aSelector];
        if ([self respondsToSelector:aSelector] || ms){
            return ms;
        }
        else{
            return [SafeProxy instanceMethodSignatureForSelector:@selector(safe_crashLog)];
        }
    }
    
    - (void)avoidCrashForwardInvocation:(NSInvocation *)anInvocation {
        
        @try {
            [self avoidCrashForwardInvocation:anInvocation];
            
        } @catch (NSException *exception) {
          //捕获异常,根据exception打印出堆栈信息,同时也避免了程序崩溃
          //上报
        } @finally {
            
        }
    }
    

    方法二:直接hook doesNotRecognizeSelector也可实现,doesNotRecognizeSelector起到抛出异常的作用,自己增加try-catch进行捕获即可,代码如下:

    [self exchangeInstanceMethod:[self class] method1Sel:@selector(doesNotRecognizeSelector:) method2Sel:@selector(avoidCrashDoesNotRecognizeSelector:)];
    
    - (void)avoidCrashDoesNotRecognizeSelector:(SEL)aSelector{
        @try {
            [self avoidCrashDoesNotRecognizeSelector:aSelector];
            
        } @catch (NSException *exception) {
           //捕获异常,根据exception打印出堆栈信息,同时也避免了程序崩溃
           //上报
        } @finally {
            
        }
    }
    

    效果如下:

    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.
    

    打印出了堆栈信息,同时避免了程序崩溃。

    注意:使用方法进行捕获异常之后,第三方工具将不会搜集到崩溃信息并上报,需要在catch中手动上报。

    参考文章

    相关文章

      网友评论

        本文标题:crash捕获及处理

        本文链接:https://www.haomeiwen.com/subject/gmuqtqtx.html