面對形形色色的奔潰問題,作為一個老碼農(nóng),從最初的不知所措,慢慢也學(xué)會了和其共存共生。畢竟奔潰抓不完,但如何更好地抓奔潰卻是個永恒的話題。從iOS發(fā)展的這數(shù)年來,關(guān)于奔潰的處理早有成熟與完整的解決方案,而此次實踐,莫如說是給這個方案再增添一些小小的裝飾罷了。
- 收集奔潰
收集崩潰大致有以下幾種方式:
A. 蘋果自帶奔潰收集系統(tǒng)。通過iTunes Connect(Manage Your Applications - View Details - Crash Reports)打開奔潰控制開關(guān),用戶同意隱私控制后即可收集奔潰。由于需要用戶主動認(rèn)可,此方式能收集的奔潰并不太多。
B. 第三方奔潰收集平臺。本人常用Fabric的Crashlytics,這個平臺的優(yōu)點在于,除收集奔潰信息外,能多維度產(chǎn)生日活,奔潰數(shù)據(jù)的日,周,月等圖線,有助于開發(fā)乃至產(chǎn)品分析。
C.自己開發(fā)的奔潰收集平臺。在NSException類提供的NSSetUncaughtExceptionHandler函數(shù)設(shè)置奔潰截獲代碼,即可在奔潰發(fā)生時執(zhí)行自定義的奔潰處理,常見的奔潰處理信息可以包含奔潰現(xiàn)場的call stack,界面信息,用戶信息,業(yè)務(wù)信息等,可視各產(chǎn)品的需要來自己定制。 - 奔潰分析
以下是Crashlytics中一段常見的奔潰日志:

常見的奔潰信息
奔潰信息包括發(fā)生時間,奔潰類型,最后停留的代碼位置及奔潰原因,以及奔潰代碼的call stack信息。
有一般經(jīng)驗的開發(fā)人員,對上面的奔潰處理應(yīng)該會比較得心應(yīng)手。這就是一個函數(shù)名無效的錯誤,原因是數(shù)據(jù)類型不是期待的NSNumber型而變成了NSNull,這類錯誤的處理應(yīng)該是比較簡單的。
那下面這個呢?



完全不知道怎么回事,有沒有?
僅有的線索:1. iOS7專享crash 2. 某一個UITextField輸入框的自動布局沒有觸發(fā) 。怎么查。如同大海撈針。
有沒有更進(jìn)一步的線索呢?其實可以有的。
當(dāng)我們做應(yīng)用埋點統(tǒng)計的時候,常常想埋得越全越好,因為產(chǎn)品總會不停得增加埋點,最后還不如一次性全覆蓋到。那奔潰日志是不是也可以參考這種模式?打印出奔潰當(dāng)時的ViewController名字怎么樣?
方式也非常簡單。ViewController的名字,可以直接通過取它的類名。獲取的時機,比較適合的是viewWillAppear,并且也可以用swizzling的方式全局獲得。當(dāng)然,如果頁面共用很多,繼承關(guān)系復(fù)雜的情況下,還是建議到每個頁面自己去獲取吧。比如:
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
//設(shè)置主線程名字,crash時記錄此name,可提高crash發(fā)現(xiàn)的幾率
NSString*className = NSStringFromClass([self class]);
if(className){
[[NSThread mainThread] setName:className];
}
}
非常簡單的代碼,就把主線程的名字替換成了當(dāng)前ViewContronller的名字。再上線抓奔潰,結(jié)果就是這樣:

是不是大大縮小了范圍。一個小小的技巧能給查奔潰帶來多大的效益呢。
- 自定義加強版的內(nèi)測奔潰收集
內(nèi)部測試時,用Crashlytics當(dāng)然也是可以的。但第三方奔潰收集在和用戶交互方面是一個短板。當(dāng)你老板在用你的應(yīng)用突然奔潰時,他的怒不可遏是可以想象的。然后他耐心的打來電話要報告這個奔潰,你卻告訴他你只能看到一堆奔潰日志,看不到他在哪個界面,操作哪個按鈕,發(fā)送的哪個請求,輸入了什么文字,反正是什么都不知道,你覺得老板年底能放過你嗎?
對于內(nèi)測用戶,稍許復(fù)雜的反饋機制是可以接受的,因為大家的目的都是為了改良產(chǎn)品。所以可以適當(dāng)增加一些反饋的信息,我們比較推薦的是在奔潰時,除常規(guī)的奔潰日志,可以增加log日志,屏幕抓圖這兩項內(nèi)容。
A. log日志的保存及獲得:
采用CocoaLumberjack這類第三方庫打印log是比較合適的方案,根據(jù)需要,CocoaLumberjack可以打印log到文件,在奔潰的時候,取log文件直接發(fā)送即可:
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,增加代碼發(fā)送log文件到奔潰平臺
}
}
B.屏幕抓圖是還原奔潰現(xiàn)場的一個有效的信息,一般奔潰平臺限于圖片文件過大,以及泄漏隱私的問題,很少提供屏幕抓圖功能。內(nèi)測環(huán)境建議自行加上奔潰時的抓圖,方便開發(fā)定位界面:
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.奔潰現(xiàn)場抓?。罕紳⑷罩究梢圆捎肗SException類,設(shè)置奔潰處理函數(shù):
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
void uncaughtExceptionHandler(NSException *exception) {
NSLog(@"%@", [NSString stringWithFormat:@"MainThread Name: %@\n%@ \n %@", [NSThread mainThread].name, exception, exception.callStackSymbols]);
}
D.發(fā)送到收集奔潰渠道
收集奔潰的渠道很多,除去那些商用的以及免費的不說,常見的可以由應(yīng)用服務(wù)器開一個接口來接收奔潰數(shù)據(jù)。這里介紹一種更適合iOS開發(fā)者以及個人的低成本的接受渠道,就是傳統(tǒng)的郵件。
通過郵件收集奔潰有不少好處,首先你不要集成那些龐大的sdk,也不用給后端提需求,只要自己默默地注冊一個郵箱。而且郵件能傳送的數(shù)據(jù)也比一般的后臺接口廣泛,文本,圖片,二進(jìn)制文件都可以。展示上也可以根據(jù)需要自由選擇頁面或者客戶端。
發(fā)送郵件通常采用SMTP協(xié)議,遺憾的是現(xiàn)在許多免費郵箱都加強了SMTP的驗證碼機制,因此網(wǎng)易,騰訊,新浪等主流郵箱已經(jīng)不能用,谷歌等被墻的更不必說,搜狐的似乎還是可以。
發(fā)送郵件我們參考了SKPSMTPMessage這個項目,并改寫了一些不能使用的方法。整個流程并不復(fù)雜,根據(jù)SMTP協(xié)議的要求,發(fā)起握手,傳輸標(biāo)題、地址等,繼續(xù)傳輸正文,附件,然后結(jié)束。
一個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
郵件發(fā)送的代碼:
#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符號表解析
通過上面方法,自己收集到的奔潰日志,都是沒有經(jīng)過解析的地址堆棧。需要轉(zhuǎn)換為函數(shù)名的堆棧信息,才能方便地找出問題所在。最方便使用的符號表解析工具是Xcode自帶的symbolicatecrash。
這個工具的使用方法已經(jīng)有很多教程,這里我們給出一個最容易記憶的方法,就是兩個素材,一個工具,一條命令。
素材1:奔潰日志文件,可以是我們自己生成的crash日志文件
素材2: dSYM文件,打包時產(chǎn)生的符號地址映射文件
工具:symbolicatecrash
命令:
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash ./*.crash ./*.app.dSYM > symbol.crash
產(chǎn)生一個新的crash日志文件,就已經(jīng)是完成符號轉(zhuǎn)換后的了。