iOS奔溃问题处理经验

面对形形色色的奔溃问题,作为一个老码农,从最初的不知所措,慢慢也学会了和其共存共生。毕竟奔溃抓不完,但如何更好地抓奔溃却是个永恒的话题。从iOS发展的这数年来,关于奔溃的处理早有成熟与完整的解决方案,而此次实践,莫如说是给这个方案再增添一些小小的装饰罢了。

  1. 收集奔溃
    收集崩溃大致有以下几种方式:
    A. 苹果自带奔溃收集系统。通过iTunes Connect(Manage Your Applications - View Details - Crash Reports)打开奔溃控制开关,用户同意隐私控制后即可收集奔溃。由于需要用户主动认可,此方式能收集的奔溃并不太多。
    B. 第三方奔溃收集平台。本人常用Fabric的Crashlytics,这个平台的优点在于,除收集奔溃信息外,能多维度产生日活,奔溃数据的日,周,月等图线,有助于开发乃至产品分析。
    C.自己开发的奔溃收集平台。在NSException类提供的NSSetUncaughtExceptionHandler函数设置奔溃截获代码,即可在奔溃发生时执行自定义的奔溃处理,常见的奔溃处理信息可以包含奔溃现场的call stack,界面信息,用户信息,业务信息等,可视各产品的需要来自己定制。
  2. 奔溃分析
    以下是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的名字。再上线抓奔溃,结果就是这样:

是不是大大缩小了范围。一个小小的技巧能给查奔溃带来多大的效益呢。

  1. 自定义加强版的内测奔溃收集
    内部测试时,用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日志文件,就已经是完成符号转换后的了。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 74,704评论 12 116
  • 前言 iOS崩溃是让iOS开发人员比较头痛的事情,app崩溃了,说明代码写的有问题,这时如何快速定位到崩溃的地方很...
    齐滇大圣阅读 47,648评论 30 384
  • 前言 崩溃是让发人员比较头痛的事情,app崩溃了,说明代码写的有问题,这时如何快速定位到崩溃的地方很重要。调试阶段...
    進无尽阅读 748评论 0 5
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 28,510评论 10 316
  • 1、第八章 Samba服务器2、第八章 NFS服务器3、第十章 Linux下DNS服务器配站点,域名解析概念命令:...
    哈熝少主阅读 1,485评论 0 5