面對形形色色的奔潰問題鸣哀,作為一個老碼農(nóng),從最初的不知所措吞彤,慢慢也學會了和其共存共生诺舔。畢竟奔潰抓不完,但如何更好地抓奔潰卻是個永恒的話題备畦。從iOS發(fā)展的這數(shù)年來,關于奔潰的處理早有成熟與完整的解決方案许昨,而此次實踐懂盐,莫如說是給這個方案再增添一些小小的裝飾罷了。
- 收集奔潰
收集崩潰大致有以下幾種方式:
A. 蘋果自帶奔潰收集系統(tǒng)糕档。通過iTunes Connect(Manage Your Applications - View Details - Crash Reports)打開奔潰控制開關莉恼,用戶同意隱私控制后即可收集奔潰。由于需要用戶主動認可速那,此方式能收集的奔潰并不太多俐银。
B. 第三方奔潰收集平臺。本人常用Fabric的Crashlytics端仰,這個平臺的優(yōu)點在于捶惜,除收集奔潰信息外,能多維度產(chǎn)生日活荔烧,奔潰數(shù)據(jù)的日吱七,周苞轿,月等圖線仿畸,有助于開發(fā)乃至產(chǎn)品分析。
C.自己開發(fā)的奔潰收集平臺。在NSException類提供的NSSetUncaughtExceptionHandler函數(shù)設置奔潰截獲代碼微饥,即可在奔潰發(fā)生時執(zhí)行自定義的奔潰處理,常見的奔潰處理信息可以包含奔潰現(xiàn)場的call stack茬斧,界面信息威蕉,用戶信息,業(yè)務信息等窜管,可視各產(chǎn)品的需要來自己定制散劫。 - 奔潰分析
以下是Crashlytics中一段常見的奔潰日志:
常見的奔潰信息
奔潰信息包括發(fā)生時間,奔潰類型微峰,最后停留的代碼位置及奔潰原因舷丹,以及奔潰代碼的call stack信息。
有一般經(jīng)驗的開發(fā)人員蜓肆,對上面的奔潰處理應該會比較得心應手颜凯。這就是一個函數(shù)名無效的錯誤,原因是數(shù)據(jù)類型不是期待的NSNumber型而變成了NSNull仗扬,這類錯誤的處理應該是比較簡單的症概。
那下面這個呢?
完全不知道怎么回事早芭,有沒有彼城?
僅有的線索:1. iOS7專享crash 2. 某一個UITextField輸入框的自動布局沒有觸發(fā) 。怎么查退个。如同大海撈針募壕。
有沒有更進一步的線索呢?其實可以有的语盈。
當我們做應用埋點統(tǒng)計的時候舱馅,常常想埋得越全越好,因為產(chǎn)品總會不停得增加埋點刀荒,最后還不如一次性全覆蓋到代嗤。那奔潰日志是不是也可以參考這種模式?打印出奔潰當時的ViewController名字怎么樣缠借?
方式也非常簡單干毅。ViewController的名字,可以直接通過取它的類名泼返。獲取的時機硝逢,比較適合的是viewWillAppear,并且也可以用swizzling的方式全局獲得。當然趴捅,如果頁面共用很多垫毙,繼承關系復雜的情況下,還是建議到每個頁面自己去獲取吧拱绑。比如:
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
//設置主線程名字综芥,crash時記錄此name,可提高crash發(fā)現(xiàn)的幾率
NSString*className = NSStringFromClass([self class]);
if(className){
[[NSThread mainThread] setName:className];
}
}
非常簡單的代碼猎拨,就把主線程的名字替換成了當前ViewContronller的名字膀藐。再上線抓奔潰,結果就是這樣:
是不是大大縮小了范圍红省。一個小小的技巧能給查奔潰帶來多大的效益呢额各。
- 自定義加強版的內測奔潰收集
內部測試時,用Crashlytics當然也是可以的吧恃。但第三方奔潰收集在和用戶交互方面是一個短板虾啦。當你老板在用你的應用突然奔潰時,他的怒不可遏是可以想象的痕寓。然后他耐心的打來電話要報告這個奔潰傲醉,你卻告訴他你只能看到一堆奔潰日志,看不到他在哪個界面呻率,操作哪個按鈕硬毕,發(fā)送的哪個請求,輸入了什么文字礼仗,反正是什么都不知道吐咳,你覺得老板年底能放過你嗎?
對于內測用戶元践,稍許復雜的反饋機制是可以接受的韭脊,因為大家的目的都是為了改良產(chǎn)品。所以可以適當增加一些反饋的信息单旁,我們比較推薦的是在奔潰時乾蓬,除常規(guī)的奔潰日志,可以增加log日志慎恒,屏幕抓圖這兩項內容。
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)場的一個有效的信息,一般奔潰平臺限于圖片文件過大节腐,以及泄漏隱私的問題外盯,很少提供屏幕抓圖功能摘盆。內測環(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)場抓缺ス丁:奔潰日志可以采用NSException類孩擂,設置奔潰處理函數(shù):
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
void uncaughtExceptionHandler(NSException *exception) {
NSLog(@"%@", [NSString stringWithFormat:@"MainThread Name: %@\n%@ \n %@", [NSThread mainThread].name, exception, exception.callStackSymbols]);
}
D.發(fā)送到收集奔潰渠道
收集奔潰的渠道很多,除去那些商用的以及免費的不說箱熬,常見的可以由應用服務器開一個接口來接收奔潰數(shù)據(jù)类垦。這里介紹一種更適合iOS開發(fā)者以及個人的低成本的接受渠道,就是傳統(tǒng)的郵件城须。
通過郵件收集奔潰有不少好處蚤认,首先你不要集成那些龐大的sdk,也不用給后端提需求糕伐,只要自己默默地注冊一個郵箱砰琢。而且郵件能傳送的數(shù)據(jù)也比一般的后臺接口廣泛,文本良瞧,圖片陪汽,二進制文件都可以。展示上也可以根據(jù)需要自由選擇頁面或者客戶端莺褒。
發(fā)送郵件通常采用SMTP協(xié)議掩缓,遺憾的是現(xiàn)在許多免費郵箱都加強了SMTP的驗證碼機制,因此網(wǎng)易遵岩,騰訊你辣,新浪等主流郵箱已經(jīng)不能用,谷歌等被墻的更不必說尘执,搜狐的似乎還是可以舍哄。
發(fā)送郵件我們參考了SKPSMTPMessage這個項目,并改寫了一些不能使用的方法誊锭。整個流程并不復雜表悬,根據(jù)SMTP協(xié)議的要求,發(fā)起握手丧靡,傳輸標題蟆沫、地址等,繼續(xù)傳輸正文温治,附件饭庞,然后結束。
一個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)過解析的地址堆棧。需要轉換為函數(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)是完成符號轉換后的了。