crash捕获及处理

       用户在使用App的过程中,经常遇到闪退的情况,体验不太好,本文尝试探索引发闪退的原因,以及在遇到crash的情况下,尽可能的保持程序运行,并及时上报错误。

一、crash类型

1.OC层面的crash

1.1 普通类型

  • NSInvalidArgumentException:非法参数异常,传入非法参数导致异常,nil参数比较常见。
  • NSRangeException:下标越界导致的异常。
  • NSGenericException: foreach的循环当中修改元素导致的异常。

1.2 KVO

KVO Crash常见原因:

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

1.3 unrecognized selector sent to instance

  • 对象接收到未知的消息,即下图中消息未能处理的情况。
    image

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

}

2.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中手动上报。

2.4 针对野指针的处理机制

模仿Xcode的zombie机制:

1.Swizzle原有allocWithZone方法,添加野指针防护标记。

2.Swizzle原有dealloc方法,如果有野指针防护标记,调用
objc_destructInstance方法,修改实例isa使其指向zombieObject,保存原始
类名,以便上报使用。

3.Swizzle消息转发机制forwardingTargetForSelector方法,处理所
有原始类originObject的方法,收集错误信息并上报。

4.及时释放zombieObject。

注: objc_destructInstance会释放与实例相关联的引用,但是并不释放该实例的内存。

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容