iOS崩潰捕捉和分析

主題: 如何捕捉發(fā)布版本ipa的崩潰, 并定位崩潰代碼

一、 崩潰日志

  • 1 什么是崩潰日志
    iOS設(shè)備上的應(yīng)用閃退時, 操作系統(tǒng)會聲稱一個崩潰日志, 保存在設(shè)備上。
路徑是:  設(shè)置 -> 隱私 ->診斷與用量 ->診斷與用量數(shù)據(jù)。在這里可以看到設(shè)備上所有的設(shè)備崩潰日志.
在“診斷與用量”界面,建議用戶選擇自動發(fā)送,這樣可以每天自動發(fā)送診斷和用量數(shù)據(jù)到itunes,來幫助開發(fā)者分析崩潰.
  • 2如何獲取崩潰日志
    2.1 連接設(shè)備獲取崩潰日志
    設(shè)備與電腦上的ITunes Store同步后, 會將崩潰日志保存在電腦上,崩潰日志保存在以下位置:
Mac OS X:   ~/Library/Logs/CrashReporter/MobileDevice/
可以看到所有和該電腦同步過的設(shè)備的崩潰日志(.crash文件)
iOS設(shè)備上的崩潰日志

2.2 通過Xcode獲取崩潰日志
打開Xcode, 菜單欄上選擇Window ->Devices,選中設(shè)備,點擊View Device Logs -> All logs可以看到所有的崩潰日志。
選中某一個崩潰日志,點擊Export Log可導出崩潰日志(.crash文件)


Xcode 查看崩潰日志

2.3 通過iTunes Connect獲取使用者上傳的崩潰日志
登錄iTunes Connect, 選中APP, 點擊可供銷售的APP(即當前最新版本), 在最下面選中額外信息下的崩潰報告, 可以看到所有iOS版本下的崩潰報告。


iTunes Connect 崩潰日志

二、iOS 崩潰日志分析

首先來看一份崩潰日志


iOS崩潰日志

(1)Incident Identifier: 是崩潰報告的唯一標識符。
(2)CrashReporter Key: 是與設(shè)備標識相對應(yīng)的唯一鍵值。雖然它不是真正的設(shè)備標識符,但也是一個非常有用的情報:如果你看到100個崩潰日志的CrashReporter Key值都是相同的,或者只有少數(shù)幾個不同的CrashReport值,說明這不是一個普遍的問題,只發(fā)生在一個或少數(shù)幾個設(shè)備上。
(3)Hardware Model: 標識設(shè)備類型。 如果很多崩潰日志都是來自相同的設(shè)備類型,說明應(yīng)用只在某特定類型的設(shè)備上有問題。上面的日志里,崩潰日志產(chǎn)生的設(shè)備是iPhone 6(但是顯示的是iPhone7,2? 暫時不清楚原因)。
(4)Process 是應(yīng)用名稱。中括號里面的數(shù)字是閃退時應(yīng)用的進程ID。
(5)Version: App版本號
最重要的兩部分
(1)Exception Type:EXC_CRASH (SIGABRT)
(2)Last Exception Backtrace(即發(fā)生崩潰的原因,也是我們要研究的重點)

Xcode會自動符號化代碼, 翻譯成明文, 如下:

Crash Logs

可以看到發(fā)生崩潰的代碼位于[SCHomePageVC viewDidLoad]方法中第408行。
崩潰的代碼是[NSArrayM insertObject:atIndex:]。
找到該行代碼,可以看到崩潰日志中所描述的崩潰發(fā)生的位置,代碼都和時機代碼一致。


崩潰的代碼

崩潰的原因是: The object to add to the array's content. This value must not be nil.

三、如何通過.crash文件反編譯得到明文的crash文件

步驟如下:
  • Step1: 在桌面上創(chuàng)建一個空的文件夾, 我將其命名為 DebugTest , 然后將三個文件放入該文件夾 "MyApp.app" , "MyApp.app.dSYM", "MyApp_2016_4_1.crash"。
  • Step2 : 打開Applications文件夾,找到 symbolicatecrash 文件, Xcode和Xcode以上,文件位置
//終端中輸入以下命令:
cd /Applications/Xcode.app/Contents/SharedFrameworks/DTDeviceKitBase.framework/Versions/A/Resources

然后你會發(fā)現(xiàn)symbolicatecrash文件,長這個樣子,將其拷貝到DebugTest文件夾中

symbolicatecrash

到這一步,你的DebugTest目錄機構(gòu)應(yīng)該是這樣
(1MyAPP.app
(2)MyApp.app.dSYM
(3)MyApp_2016_4_1.crash
(4)symbolicatecrash

  • Step3: 在終端中輸入以下3條命令
//第一條命令(其中Yourname 應(yīng)該是你的用戶名)
 cd /Users/Yourname/Desktop/DebugTest
// 第二條命令
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
//第三條命令(二選一)
(Xcode6.3和之前版本輸入以下命令)
./symbolicatecrash -A -v MYApp_2016-4-1.crash MyApp.app.dSYM
(Xcode6.4和之前版本輸入以下命令)
./symbolicatecrash  -v MYApp_2016-4-1.crash MyApp.app.dSYM

然后用控制臺打開你的MyApp_2016_4_1.crash文件, 你就會看到編譯后的crash文件, 同Xcode看到的崩潰日志一致。通過查看崩潰日志,可以輕易的找到崩潰原因并修正。

Crash Logs

四、如何在程序崩潰時手動捕捉到崩潰

   當我們debug的時候, 發(fā)生崩潰后可以在控制臺上看到崩潰的堆棧信息和崩潰日志。上面三種方法都是我們獲取.crash文件后解析的辦法, 那么如果用戶不發(fā)送崩潰日志到iTunes Connect時,我們?nèi)绾潍@取崩潰信息呢?(盡可能的獲取崩潰信息有助于熱修復時定位代碼)。當然,友盟支持搜集崩潰日志,那我們是否也可以在程序崩潰時,將崩潰信息寫入本地,APP再次啟動時,將崩潰信息上傳到我們的服務(wù)器。這里就要用到apple的一個函數(shù):NSSetUncaughtExceptionHandler。上代碼:
//application didFinishLaunchingWithOptions中調(diào)用 [self catchCrashLogs];
  
- (void)catchCrashLogs{
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
void UncaughtExceptionHandler(NSException *exception){
    if (exception ==nil)return;
    NSArray *array = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name  = [exception name];
    NSDictionary *dict = @{@"appException":@{@"exceptioncallStachSymbols":array,@"exceptionreason":reason,@"exceptionname":name}};
    if([SDFileToolClass writeCrashFileOnDocumentsException:dict]){
        NSLog(@"Crash logs write ok!");
    }
}
//寫入緩存中: 以下提供三個API,分別是:寫入,獲取,清空
NSString * const SDCrashFileDirectory = @"SDMapHomeCrashFileDirectory"; //你的項目中自定義文件夾名
+ (NSString *)sd_getCachesPath{
    return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
}
+ (BOOL)writeCrashFileOnDocumentsException:(NSDictionary *)exception{
    NSString *time = [[NSDate date] formattedDateWithFormat:@"yyyyMMddHHmmss" locale:[NSLocale currentLocale]];
    NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
    NSString *crashname = [NSString stringWithFormat:@"%@_%@Crashlog.plist",time,infoDictionary[@"CFBundleName"]];
    NSString *crashPath = [[self sd_getCachesPath] stringByAppendingPathComponent:SDCrashFileDirectory];
    NSFileManager *manager = [NSFileManager defaultManager];
    //設(shè)備信息
    NSMutableDictionary *deviceInfos = [NSMutableDictionary dictionary];
    [deviceInfos setObject:[infoDictionary objectForKey:@"DTPlatformVersion"] forKey:@"DTPlatformVersion"];
    [deviceInfos setObject:[infoDictionary objectForKey:@"CFBundleShortVersionString"] forKey:@"CFBundleShortVersionString"];
    [deviceInfos setObject:[infoDictionary objectForKey:@"UIRequiredDeviceCapabilities"] forKey:@"UIRequiredDeviceCapabilities"];
    
    BOOL isSuccess = [manager createDirectoryAtPath:crashPath withIntermediateDirectories:YES attributes:nil error:nil];
    if (isSuccess) {
        NSLog(@"文件夾創(chuàng)建成功");
        NSString *filepath = [crashPath stringByAppendingPathComponent:crashname];
        NSMutableDictionary *logs = [NSMutableDictionary dictionaryWithContentsOfFile:filepath];
        if (!logs) {
            logs = [[NSMutableDictionary alloc] init];
        }
        //日志信息
        NSDictionary *infos = @{@"Exception":exception,@"DeviceInfo":deviceInfos};
        [logs setObject:infos forKey:[NSString stringWithFormat:@"%@_crashLogs",infoDictionary[@"CFBundleName"]]];
        BOOL writeOK = [logs writeToFile:filepath atomically:YES];
        NSLog(@"write result = %d,filePath = %@",writeOK,filepath);
        return writeOK;
    }else{
        return NO;
    }
}
+ (nullable NSArray *)sd_getCrashLogs{
     NSString *crashPath = [[self sd_getCachesPath] stringByAppendingPathComponent:SDCrashFileDirectory];
     NSFileManager *manager = [NSFileManager defaultManager];
     NSArray *array = [manager contentsOfDirectoryAtPath:crashPath error:nil];
     NSMutableArray *result = [NSMutableArray array];
    if (array.count == 0) return nil;
    for (NSString *name in array) {
        NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:[crashPath stringByAppendingPathComponent:name]];
        [result addObject:dict];
    }
    return result;
}
+ (BOOL)sd_clearCrashLogs{
     NSString *crashPath = [[self sd_getCachesPath] stringByAppendingPathComponent:SDCrashFileDirectory];
    NSFileManager *manager = [NSFileManager defaultManager];
    if (![manager fileExistsAtPath:crashPath]) return YES; //如果不存在,則默認為刪除成功
    NSArray *contents = [manager contentsOfDirectoryAtPath:crashPath error:NULL];
    if (contents.count == 0) return YES;
    NSEnumerator *enums = [contents objectEnumerator];
    NSString *filename;
    BOOL success = YES;
    while (filename = [enums nextObject]) {
        if(![manager removeItemAtPath:[crashPath stringByAppendingPathComponent:filename] error:NULL]){
            success = NO;
            break;
        }
    }
    return success;
}

Well done!

五、結(jié)論: 為了更好的分析崩潰原因,在每次上架APP的時候,應(yīng)該保留對應(yīng)的app文件和dsym文件。

六、參考鏈接:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容