崩溃信息的收集
之前有写过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
网友评论