面对形形色色的奔溃问题,作为一个老码农,从最初的不知所措,慢慢也学会了和其共存共生。毕竟奔溃抓不完,但如何更好地抓奔溃却是个永恒的话题。从iOS发展的这数年来,关于奔溃的处理早有成熟与完整的解决方案,而此次实践,莫如说是给这个方案再增添一些小小的装饰罢了。
- 收集奔溃
收集崩溃大致有以下几种方式:
A. 苹果自带奔溃收集系统。通过iTunes Connect(Manage Your Applications - View Details - Crash Reports)打开奔溃控制开关,用户同意隐私控制后即可收集奔溃。由于需要用户主动认可,此方式能收集的奔溃并不太多。
B. 第三方奔溃收集平台。本人常用Fabric的Crashlytics,这个平台的优点在于,除收集奔溃信息外,能多维度产生日活,奔溃数据的日,周,月等图线,有助于开发乃至产品分析。
C.自己开发的奔溃收集平台。在NSException类提供的NSSetUncaughtExceptionHandler函数设置奔溃截获代码,即可在奔溃发生时执行自定义的奔溃处理,常见的奔溃处理信息可以包含奔溃现场的call stack,界面信息,用户信息,业务信息等,可视各产品的需要来自己定制。 - 奔溃分析
以下是Crashlytics中一段常见的奔溃日志:
常见的奔溃信息
奔溃信息包括发生时间,奔溃类型,最后停留的代码位置及奔溃原因,以及奔溃代码的call stack信息。
有一般经验的开发人员,对上面的奔溃处理应该会比较得心应手。这就是一个函数名无效的错误,原因是数据类型不是期待的NSNumber型而变成了NSNull,这类错误的处理应该是比较简单的。
那下面这个呢?
完全不知道怎么回事,有没有?
仅有的线索:1. iOS7专享crash 2. 某一个UITextField输入框的自动布局没有触发 。怎么查。如同大海捞针。
有没有更进一步的线索呢?其实可以有的。
当我们做应用埋点统计的时候,常常想埋得越全越好,因为产品总会不停得增加埋点,最后还不如一次性全覆盖到。那奔溃日志是不是也可以参考这种模式?打印出奔溃当时的ViewController名字怎么样?
方式也非常简单。ViewController的名字,可以直接通过取它的类名。获取的时机,比较适合的是viewWillAppear,并且也可以用swizzling的方式全局获得。当然,如果页面共用很多,继承关系复杂的情况下,还是建议到每个页面自己去获取吧。比如:
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
//设置主线程名字,crash时记录此name,可提高crash发现的几率
NSString*className = NSStringFromClass([self class]);
if(className){
[[NSThread mainThread] setName:className];
}
}
非常简单的代码,就把主线程的名字替换成了当前ViewContronller的名字。再上线抓奔溃,结果就是这样:
是不是大大缩小了范围。一个小小的技巧能给查奔溃带来多大的效益呢。
- 自定义加强版的内测奔溃收集
内部测试时,用Crashlytics当然也是可以的。但第三方奔溃收集在和用户交互方面是一个短板。当你老板在用你的应用突然奔溃时,他的怒不可遏是可以想象的。然后他耐心的打来电话要报告这个奔溃,你却告诉他你只能看到一堆奔溃日志,看不到他在哪个界面,操作哪个按钮,发送的哪个请求,输入了什么文字,反正是什么都不知道,你觉得老板年底能放过你吗?
对于内测用户,稍许复杂的反馈机制是可以接受的,因为大家的目的都是为了改良产品。所以可以适当增加一些反馈的信息,我们比较推荐的是在奔溃时,除常规的奔溃日志,可以增加log日志,屏幕抓图这两项内容。
A. log日志的保存及获得:
采用CocoaLumberjack这类第三方库打印log是比较合适的方案,根据需要,CocoaLumberjack可以打印log到文件,在奔溃的时候,取log文件直接发送即可:
NSArray *loggers = [DDLog allLoggers];
for (id logger in loggers){
if ([logger isKindOfClass:[DDFileLogger class]]){
NSString *logPath = ((DDFileLogger *)logger).logFileManager.logsDirectory;
NSData *logData = [NSData dataWithContentsOfFile:logPath];
//ToDo,增加代码发送log文件到奔溃平台
}
}
B.屏幕抓图是还原奔溃现场的一个有效的信息,一般奔溃平台限于图片文件过大,以及泄漏隐私的问题,很少提供屏幕抓图功能。内测环境建议自行加上奔溃时的抓图,方便开发定位界面:
UIGraphicsBeginImageContext([UIScreen mainScreen].bounds.size);
UIGraphicsBeginImageContextWithOptions([UIScreen mainScreen].bounds.size, NO, 0.0);
[self.window.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *libraryPath = [paths objectAtIndex:0];
NSString *path = [libraryPath stringByAppendingPathComponent:@"crashSnap.jpg"];
[UIImageJPEGRepresentation(image, 1.0) writeToFile:path atomically:YES];
C.奔溃现场抓取:奔溃日志可以采用NSException类,设置奔溃处理函数:
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
void uncaughtExceptionHandler(NSException *exception) {
NSLog(@"%@", [NSString stringWithFormat:@"MainThread Name: %@\n%@ \n %@", [NSThread mainThread].name, exception, exception.callStackSymbols]);
}
D.发送到收集奔溃渠道
收集奔溃的渠道很多,除去那些商用的以及免费的不说,常见的可以由应用服务器开一个接口来接收奔溃数据。这里介绍一种更适合iOS开发者以及个人的低成本的接受渠道,就是传统的邮件。
通过邮件收集奔溃有不少好处,首先你不要集成那些庞大的sdk,也不用给后端提需求,只要自己默默地注册一个邮箱。而且邮件能传送的数据也比一般的后台接口广泛,文本,图片,二进制文件都可以。展示上也可以根据需要自由选择页面或者客户端。
发送邮件通常采用SMTP协议,遗憾的是现在许多免费邮箱都加强了SMTP的验证码机制,因此网易,腾讯,新浪等主流邮箱已经不能用,谷歌等被墙的更不必说,搜狐的似乎还是可以。
发送邮件我们参考了SKPSMTPMessage这个项目,并改写了一些不能使用的方法。整个流程并不复杂,根据SMTP协议的要求,发起握手,传输标题、地址等,继续传输正文,附件,然后结束。
一个SMTP传输示例:
S: 220 www.example.com ESMTP Postfix
C: HELO mydomain.com
S: 250 Hello mydomain.com
C: MAIL FROM: <sender@mydomain.com>
S: 250 Ok
C: RCPT TO: <friend@example.com>
S: 250 Ok
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: Subject: test message
C: From:""< sender@mydomain.com>
C: To:""< friend@example.com>
C:
C: Hello,
C: This is a test.
C: Goodbye.
C: .
S: 250 Ok: queued as 12345
C: quit
S: 221 Bye
邮件发送的代码:
#import "MailSender.h"
@interface PBCrashReporter () <MailSenderDelegate>
@end
@implementation PBCrashReporter
- (void)sendFeedbackEmail
{
MailSender *mailSender = [[MailSender alloc] init];
mailSender.fromEmail = @"xxx@sohu.com";
mailSender.toEmail = @"xxx@sohu.com";
mailSender.relayHost = @"smtp.sohu.com";
mailSender.requiresAuth = YES;
mailSender.login = @"xxx@sohu.com";
mailSender.pass = @"xxxxxx";
mailSender.wantsSecure = NO;
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSString *userId = [defaults stringForKey:kUserId];
if (userId){
mailSender.fromName = userId;
}
mailSender.subject = @"奔溃收集邮件";
mailSender.delegate = self;
NSDictionary *plainPart = [NSDictionary dictionaryWithObjectsAndKeys:@"text/plain; charset=UTF-8",smtpPartContentTypeKey,
@"crash日志,详情见附件",smtpPartMessageKey,@"8bit",smtpPartContentTransferEncodingKey,nil];
NSString *vcf1Path = [PBCrashReporter pathOfReportFile];
NSData *vcf1Data = [NSData dataWithContentsOfFile:vcf1Path];
NSDictionary *vcf1Part = [NSDictionary dictionaryWithObjectsAndKeys:@"text/directory;\r\n\tx-unix-mode=0644;\r\n\tname=\"crash.txt\"",smtpPartContentTypeKey,
@"attachment;\r\n\tfilename=\"crash.txt\"",smtpPartContentDispositionKey,[vcf1Data base64EncodedStringWithOptions:0],smtpPartMessageKey,@"base64",smtpPartContentTransferEncodingKey,nil];
mailSender.parts = [NSArray arrayWithObjects:plainPart,vcf1Part,vcf2Part,nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[mailSender sendMail];
});
}
- (void)mailSent:(JFMailSender *)message
{
//if something must run in main thread,please use dispatch_get_main_queue();
NSLog(@"Yay! Message was sent!");
[[NSFileManager defaultManager] removeItemAtPath:[PBCrashReporter pathOfReportFile] error:nil];
[[NSFileManager defaultManager] removeItemAtPath:[PBCrashReporter pathOfSnapFile] error:nil];
}
- (void)mailFailed:(JFMailSender *)message error:(NSError *)error
{
//if something must run in main thread,please use dispatch_get_main_queue();
NSLog(@"%@", [NSString stringWithFormat:@"Darn! Error!\n%li: %@\n%@", (long)[error code], [error localizedDescription], [error localizedRecoverySuggestion]]);
}
@end
crash符号表解析
通过上面方法,自己收集到的奔溃日志,都是没有经过解析的地址堆栈。需要转换为函数名的堆栈信息,才能方便地找出问题所在。最方便使用的符号表解析工具是Xcode自带的symbolicatecrash。
这个工具的使用方法已经有很多教程,这里我们给出一个最容易记忆的方法,就是两个素材,一个工具,一条命令。
素材1:奔溃日志文件,可以是我们自己生成的crash日志文件
素材2: dSYM文件,打包时产生的符号地址映射文件
工具:symbolicatecrash
命令:
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash ./*.crash ./*.app.dSYM > symbol.crash
产生一个新的crash日志文件,就已经是完成符号转换后的了。