美文网首页
通过UncaughtExceptionHandler收集App的

通过UncaughtExceptionHandler收集App的

作者: 翻炒吧蛋滚饭 | 来源:发表于2018-05-25 16:25 被阅读112次

崩溃信息的收集

  之前有写过Bugly的集成以及一些使用方法 使用Bugly收集并分析App的崩溃信息 ,当然任何三方工具,如果只是拿来用的话,无非是注册下账号->拖动SDK到项目中->按照文档设置下启动方式&配置特性就OK了,但Bugly是怎么收集App的Crash信息的呢?最近有关注了下这方面的知识点,把自己整理的一些资料分享出来,当然主要参考了很多别人的博客,但因为很多文章要么注释不全,要么逻辑混乱没有Demo,而且还有一些操作是走不通的,所以这里整理一份Demo,在文章的结尾放出,如果有侵权,请告知。

警告,阅前必读!!!

  当然Bugly处理Crash更方便、更专业,我们只是通过该文章来了解背后的实现机制,请不要把接下来讲的东西使用在项目中,也不要为了防止App崩溃而使用此方法,毕竟让用户知道App会在某些操作下闪退,才知道问题发生在哪里,同时我们做了下面的一些操作后,会覆盖Bugly的一些设置,导致Bugly失效!!!

NSSetUncaughtExceptionHandler

  Foundation框架中为我们提供了一些方法来捕获NSException:

typedef void NSUncaughtExceptionHandler(NSException *exception);

FOUNDATION_EXPORT NSUncaughtExceptionHandler * _Nullable NSGetUncaughtExceptionHandler(void);
FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);

  最后一行的NSSetUncaughtExceptionHandler函数接受一个NSUncaughtExceptionHandler类型的参数,这个参数是一个C函数,参数是一个NSException。
  我们只需要在App启动时候(AppDelegate中)调用NSSetUncaughtExceptionHandler就好了,并传入我们自己写好的HandleException函数(C语言):

NSSetUncaughtExceptionHandler(&HandleException);

  当系统抛出NSException的时候,因为我们已经将自己的HandleException函数传给系统作为UncaughtExceptionHandler了,所以这时系统就会调用我们的函数,并将这个Exception作为参数传给我们定义的函数:

// 截获异常信息
void HandleException(NSException *exception) {
    // 渠道回溯的堆栈
    NSArray *callStack = [UncaughtExceptionHandler backtrace];
    NSMutableDictionary *userInfo =[NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
    // 将堆栈信息保存到userInfo中
    [userInfo setObject:callStack forKey:UncaughtExceptionHandlerAddressesKey];
    
    // 封装一个新的NSException,让我们的UncaughtExceptionHandler去处理
    [[[UncaughtExceptionHandler alloc] init]performSelectorOnMainThread:@selector(handleAnException:) withObject:
     [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo] waitUntilDone:YES];
}

  这里用到了一个我们自定义的类UncaughtExceptionHandler,同时使用了这个类的中两个方法,一个类方法:backtrace,一个实例方法:handleAnException。这里看一下我们定义的类中的一些方法:

@implementation UncaughtExceptionHandler

// 获取调用堆栈
+ (NSArray *)backtrace {
    // 指针列表
    void *callstack[128];
    
    // backtrace用来获取当前线程的调用堆栈,获取的信息存放在这里的callstack中
    // 128用来指定当前的buffer中可以保存多少个void*元素
    // 返回值是实际获取的指针个数
    int frames = backtrace(callstack, 128);
    
    // backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组
    // 返回一个指向字符串数组的指针
    // 每个字符串包含了一个相对于callstack中对应元素的可打印信息,包括函数名、偏移地址、实际返回地址
    char **strs = backtrace_symbols(callstack, frames);
    int i;
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (i = UncaughtExceptionHandlerSkipAddressCount; i <UncaughtExceptionHandlerSkipAddressCount +UncaughtExceptionHandlerReportAddressCount; i++){
        [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);     // C中的类型记得free
    return backtrace;
}

- (void)handleAnException:(NSException *)exception {
    // 做一些崩溃前的处理(比如弹个窗啥的)
    [self validateAndSaveCriticalApplicationDataWithException:exception];
    
    
    
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

    while (![self.dimissed integerValue]) {
        for (NSString *mode in (__bridge NSArray *)allModes) {
            // 为阻止线程退出,使用 CFRunLoopRunInMode(model, 0.001, false)等待系统消息,false表示RunLoop没有超时时间
            CFRunLoopRunInMode((CFStringRef)mode,0.001, false);
        }
    }
    
    
    

    CFRelease(allModes);
    NSSetUncaughtExceptionHandler(NULL);
 
    NSLog(@"%@",[exception name]);
    if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName]) {
        kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
    }else{
        [exception raise];
    }
}


// 自己定义一个处理崩溃的方法,弹个alert啥的
- (void)validateAndSaveCriticalApplicationDataWithException:(NSException *)exception {
    /******************************** 展示崩溃信息 ********************************/
    NSLog(@"崩溃了");
    
    // 将Eexception的name、reason、userInfo(堆栈信息)展示出来
    NSString *message = [NSString stringWithFormat:NSLocalizedString(@"请截图发送给开发者,谢谢配合\n异常原因如下:\n%@\n%@\n%@",nil), [exception name], [exception reason],[[exception userInfo] objectForKey:UncaughtExceptionHandlerAddressesKey]];
    
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"闪退了"  message:message preferredStyle:UIAlertControllerStyleAlert];
    
    // 缩小message字体,保证展示尽可能多的崩溃信息
    NSMutableAttributedString *alertControllerMessageStr = [[NSMutableAttributedString alloc] initWithString:message attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:5]}];
    if ([alert valueForKey:@"attributedMessage"]) {
        [alert setValue:alertControllerMessageStr forKey:@"attributedMessage"];
    }

    __weak typeof(self) _ws = self;
    UIAlertAction *continueAction = [UIAlertAction actionWithTitle:@"继续运行" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    }];
    [alert addAction:continueAction];

    UIViewController * rootViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
    [rootViewController presentViewController:alert animated:NO completion:nil];
}

@end

  这里会发现backtrace方法获取到了崩溃线程堆栈信息,HandleException方法将堆栈信息、Exception的name/reason又重新包装成一个新的NSException交给我们的UncaughtExceptionHandler的一个实例的handleAnException方法进行处理,handleAnException方法拿到Exception后可以通过一个弹窗告知用户,正常情况下,当发生错误后,线程就会退出了,但我们可以通过保持Runloop持续运行的方式,阻止线程退出(可能理解有误!)。
  我们可以用过@[][1]来模拟下数组越界,然后会发现崩溃信息如下:

  当点击了弹窗的确定后,App就可以继续运行了。不过这个方法只能拦截到一次崩溃,当下一次App发生错误的时候,我们的HandleException就不走了,如果有小伙伴知道怎么解决这个问题,希望可以告诉我怎么解决,非常感谢!

意义

  那么我们磨磨唧唧的讲了这么多、粘了这么多代码,这个知识点有什么用呢?当然对于我们的实际开发来说用处不大,因为在友盟统计、Bugly提供了强大的三方支持后,我们没有必要去自己捕获Crash信息、自己上传服务器进行统计、自己根据dSYM文件找到崩溃所在类、代码行数。但是我们不能因为有了别人的帮助,就不去关注背后的实现原理了,因为看完文章后你会发现,其实想实现一个Crash信息收集工具,并没有想象中的那么难。
  由于篇幅有限,文章中没有贴出所有代码,所以直接复制粘贴是跑不起来的,也不便于理解,所以我把代码放到了Git上获取Demo,主要看下面几个文件就好了:


  NSException是可以捕获的,但系统的一些其他崩溃,比如野指针什么的,系统是通过Signal的形式发出来然后让App崩溃的,这部分的错误捕获也写在Demo中了,但并没有生效,这个还有待研究。还有代码如何支持Swift,可以看这个Swift支持,我没有试验是否有效,在做Swift项目且时间充裕的同学,可以尝试下。
  如果讲的有什么不对,欢迎指出!不希望把错误的东西教给不懂这方面知识的人,如果有更好的改进方法,也请教给我,非常感谢了!获取Demo

相关文章

  • 通过UncaughtExceptionHandler收集App的

    崩溃信息的收集   之前有写过Bugly的集成以及一些使用方法 使用Bugly收集并分析App的崩溃信息 ,当然任...

  • iOS 收集APP崩溃

    UncaughtExceptionHandler 业务场景描述 收集APP崩溃信息,上传到服务器,用于分析统计. ...

  • iOS-使用UncaughtExceptionHandler收集

    详细代码如下: 一句话调用: 源码下载:文件里面的自定义Bug收集

  • UncaughtExceptionHandler,用户使用App

    转载请注明原创出处,谢谢! GitHub: @Ricco 辛辛苦苦、日夜兼程写了15天,总算写了一个小项目,如何让...

  • Android全局异常捕获

    全局异常捕获 UncaughtExceptionHandler 当app上线后,可能存在某些异常导致程序崩溃,开...

  • FileObserver捕获ANR异常

    Android中捕获运行时异常,可以通过继承UncaughtExceptionHandler来重写uncaught...

  • UncaughtExceptionHandler

    目录 简介 XJB 代码 用法 简介 UncaughtExceptionHandler 是一个接口,通过它可以捕获...

  • 系统异常捕获存储上传功能实现

    UncaughtExceptionHandler是全局捕获异常的,为app意外中止的提供一些处理的方法。只需要实现...

  • 小说简单 隐私策略

    本隐私政策介绍本APP的隐私数据相关政策和惯例,这将涵盖我们如何收集、使用、处理、存储和/或披露那些通过App收集...

  • 隐私政策

    前言 本条例是介绍本公司的隐私数据相关政策和惯例,这将涵盖我们如何收集、使用、处理那些通过本公司的移动App收集的...

网友评论

      本文标题:通过UncaughtExceptionHandler收集App的

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