iOS中Crash采集及PLCrashReporter使用

本文整理下最近對于crash采集的總結,和踩過的坑。

CrashReporter


首先,iOS有自己的CrashReporter機制。在真機上產生的crash,在一下兩個地方可以找到:

  • Xcode-Window-Devices - View Device Logs中可以看到crash文件。這是我的截圖:


    QQ20170125-0@2x.png

    關于各個字段的含義,我搜集了相關博客的介紹,有不對的地方大家可以指出:

字段 含義
Incident Identifier 當前crash的 id,可以區分不同的crash事件
CrashReporter Key 當前設備的id,可以判斷crash在某一設備上出現的頻率
Hardware Model 設備型號
Process 當前應用的名稱,后面中括號中為當前的應用在系統中的進程id
Path 當前應用在設備中的路徑
Identifier bundle id
Version 應用版本號
Code Type 還不清楚
Date/Time crash事件 時間(后面跟的應該是時區)
OS Version 當前系統版本
Exception Type 異常類型
Exception Codes 異常出錯的代碼(常見代碼有以下幾種)
0x8badf00d錯誤碼:Watchdog超時,意為“ate bad food”。
0xdeadfa11錯誤碼:用戶強制退出,意為“dead fall”。
0xbaaaaaad錯誤碼:用戶按住Home鍵和音量鍵,獲取當前內存狀態,不代表崩潰。
0xbad22222錯誤碼:VoIP應用(因為太頻繁?)被iOS干掉。
0xc00010ff錯誤碼:因為太燙了被干掉,意為“cool off”。
0xdead10cc錯誤碼:因為在后臺時仍然占據系統資源(比如通訊錄)被干掉,意為“dead lock”。
Triggered by Thread 在某一個線程出了問題導致crash,Thread 0 為主線程、其它的都為子線程
Last Exception Backtrace 最后異常回溯,一般根據這個代碼就能找到crash的具體問題
  • 通過iTunes Connect(Manage Your Applications - View Details - Crash Reports)獲取用戶的crash日志。需要用戶在設置-診斷與用量中允許將崩潰信息發送給開發者。然后在也可以在Xcode的Window - Organizer中可以看到對應的crash信息。(需要在Xcode中登錄所屬的開發者賬號)


    QQ20170125-1@2x.png

.dSYM文件

這是你拿到的crash日志:

QQ20170213-0@2x.png

取到的crash文件的崩潰信息會是地址信息,這時候需要使用打包時對應的dSYM文件進行符號表的解析工作,所以每次生產版本打包時,都需要保存對應的dSYM文件,一些第三方的crash采集分析平臺也會要求上傳對應的dSYM文件。
解析需要用到Xcode中一個symbolicatecrash的程序。目錄地址在

/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
如果嫌麻煩,也可以直接輸入命令
find /Applications/Xcode.app -name symbolicatecrash -type f
將symbolicatecrash拷貝到crash文件,dSYM文件相同的目錄中。

進入所在目錄
cd /Users/username/Desktop/CrashReport
依次執行以下的命令即可輸出為目標文件symbol.crash

export DEVELOPER_DIR=/Applications/XCode.app/Contents/Developer
./symbolicatecrash ./*.crash ./*.app.dSYM > symbol.crash
以上這種獲取crash信息的方式不夠滿足我們產品的需要,想通過用戶主動上傳或者同意發送崩潰信息存在太多的困難。

依靠程序實現crash的捕捉


在搜索相關資料的時候,比較常見的方式分兩種。

異常處理機制

同時對于系統Crash而引起的程序異常退出,可以通過UncaughtExceptionHandler機制捕獲;

也就是說在程序中catch以外的內容,被系統自帶的錯誤處理而捕獲。我們要做的就是用自定義的函數替代該ExceptionHandler即可。
這里主要有兩個函數

NSGetUncaughtExceptionHandler() 得到現在系統自帶處理Handler;得到它后,如果程序正常退出時用來回復系統原先設置
NSSetUncaughtExceptionHandler() 紅色設置自定義的函數

該方式可以捕捉到常見的數組越界等OC層面拋出的異常。
PS:在設置handler時需要注意一點。在念茜的《漫談iOS Crash收集框架》中提到

如果同時有多方通過NSSetUncaughtExceptionHandler注冊異常處理程序,和平的作法是:后注冊者通過NSGetUncaughtExceptionHandler將先前別人注冊的handler取出并備份,在自己handler處理完后自覺把別人的handler注冊回去,規規矩矩的傳遞。不傳遞強行覆蓋的后果是,在其之前注冊過的日志收集服務寫出的Crash日志就會因為取不到NSException而丟失Last Exception Backtrace等信息。(P.S. iOS系統自帶的Crash Reporter不受影響)

建議在自己的handle處理完之后,設置回原先保存的別人注冊的handler

處理signal

除了OC層面的異常捕捉之外,很多內存錯誤、訪問錯誤的地址產生的crash則需要利用unix標準的signal機制,注冊SIGABRT, SIGBUS, SIGSEGV等信號發生時的處理函數。該函數中我們可以輸出棧信息,版本信息等其他一切我們所想要的。

實例代碼:

void SignalExceptionHandler(int signal)
{
    NSMutableString *mstr = [[NSMutableString alloc] init];
    
    [mstr appendString:@"Stack:\n"];
    
    void* callstack[128];
    
    int i, frames = backtrace(callstack, 128);
    
    char** strs = backtrace_symbols(callstack, frames);
    
    for (i = 0; i<frames;i++)
    {
         [mstr appendFormat:@"%s\n", strs[i]];
         
    }   //[ saveCreash:mstr];
}
void InstallSignalHandler(void)
{
    signal(SIGHUP, SignalExceptionHandler);
    signal(SIGINT, SignalExceptionHandler);
    signal(SIGQUIT, SignalExceptionHandler);
    signal(SIGABRT, SignalExceptionHandler);
    signal(SIGILL, SignalExceptionHandler);
    signal(SIGSEGV, SignalExceptionHandler);
    signal(SIGFPE, SignalExceptionHandler);
    signal(SIGBUS, SignalExceptionHandler);
    signal(SIGPIPE, SignalExceptionHandler);
}

關于這塊,雖說能找到很多類似的、相互轉載的資料,但是大部分的代碼都多多少少有問題,沒有奏效。放個最后找到的可以用的地址
關于上述提到的多方通過NSSetUncaughtExceptionHandler注冊異常時候的處理,所以我把這步優化加上了。我的demo

ps:關于signal信號的捕捉,在Xcode調試時,Debugger模式會先于我們的代碼catch到所有的crash,所以需要直接從模擬器中進入程序才可以測試

相關開源庫的實現


至此,簡單的crash采集工作基本算是完成了,能一定程度上滿足對于crash日志信息采集的需求了,也能從信息中定位到問題所在。

但是這種方式獲取到的日志信息(指signal信號捕捉的信息)有簡單的崩潰堆棧信息,不需要進行符號表的反解。
并且我查看了某個平臺的crash文件格式,上文說到平臺需要提前上傳dSYM文件。文件格式和系統生成的crash文件基本一致,該有的字段信息都有。所以相關實現肯定是不一樣的,在翻閱頭文件的時候看到了#import <mach/mach.h>,回想起上文提到的念茜去年的一篇博客 -《漫談iOS Crash收集框架》。之前看的時候,云里霧里,現在稍許有些概念。

所有Mach異常都在host層被ux_exception轉換為相應的Unix信號,并通過threadsignal將信號投遞到出錯的線程。iOS中的 POSIX API 就是通過 Mach 之上的 BSD 層實現的。

因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach層的EXC_BAD_ACCESS異常,在host層被轉換成SIGSEGV信號投遞到出錯的線程。既然最終以信號的方式投遞到出錯的線程,那么就可以通過注冊signalHandler來捕獲信號:

signal(SIGSEGV,signalHandler);

捕獲Mach異常或者Unix信號都可以抓到crash事件,這兩種方式哪個更好呢?

優選Mach異常,因為Mach異常處理會先于Unix信號處理發生,如果Mach異常的handler讓程序exit了,那么Unix信號就永遠不會到達這個進程了。轉換Unix信號是為了兼容更為流行的POSIX標準(SUS規范),這樣不必了解Mach內核也可以通過Unix信號的方式來兼容開發。

猜測就是通過mach的相關接口獲取到崩潰信息的。于是去github上找了相關的開源KSCrashplcrashreporter。確實這兩個庫中都得到了對應上述的crash文件中大部分的信息。于是開始著手plcrashreporter的集成使用。


plcrashreporter

集成

作者在工程里新建了多個target,對應模擬器的.a庫、iOS的.a庫、iOS的framework、Mac的framework等。對framework也做了模擬器和真機版本的合并操作。直接將對應的framework拖入到自己工程中使用就可以了。
相關的集成代碼包括:

// 是的調試模式下是無法獲取到crash信息的 作者直接讓demo退出了
 if (debugger_should_exit()) {
        NSLog(@"The demo crash app should be run without a debugger present. Exiting ...");
        return 0;
    }
    
    /* Configure our reporter */
    PLCrashReporterConfig *config = [[[PLCrashReporterConfig alloc] initWithSignalHandlerType: PLCrashReporterSignalHandlerTypeMach
                                                                        symbolicationStrategy: PLCrashReporterSymbolicationStrategyAll] autorelease];
    PLCrashReporter *reporter = [[[PLCrashReporter alloc] initWithConfiguration: config] autorelease];

    /* Save any existing crash report. */
    // demo每次啟動會把上次的crash日志拷貝到document目錄下,并且開啟了itunes的共享
    save_crash_report(reporter);
    
    /* Set up post-crash callbacks */
    PLCrashReporterCallbacks cb = {
        .version = 0,
        .context = (void *) 0xABABABAB,
        .handleSignal = post_crash_callback
    };
    [reporter setCrashCallbacks: &cb];

    /* Enable the crash reporter */
    // 開啟crashrepoter
    if (![reporter enableCrashReporterAndReturnError: &error]) {
        NSLog(@"Could not enable crash reporter: %@", error);
    }

    /* Add another stack frame */
    // demo制造的一個crash
    stackFrame();

解析

在沙盒的library-cache中保存了一個plcrash格式的文件,如何使用這個文件。作者提供了一個CrashViewer的Mac程序來打開。所以在集成后,可以自己添加plcrash的解析,寫成log格式到本地,進行自己的上報操作。在工具中可以看到主要的解析代碼是:

- (BOOL) readFromData: (NSData *)data ofType: (NSString *)typeName error: (__autoreleasing NSError **)outError
{
    if ([typeName isEqual: @"PLCrash"]) {
        PLCrashReport *report = [[PLCrashReport alloc] initWithData: data error: outError];
        if (!report)
            return NO;

        NSString *text = [PLCrashReportTextFormatter stringValueForCrashReport: report
                                                                withTextFormat: PLCrashReportTextFormatiOS];
        self.reportText = text;
        return YES;
    } else if ([typeName isEqual: @"com.apple.crashreport"] || [typeName isEqual: @"public.plain-text"]) {
        NSString *text = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
        self.reportText = text;
        return text != nil;
    }
    return NO;
}

我原本是想通過程序菜單欄中的
菜單欄

file按鈕找到對應的處理函數,但是竟然找不到對應的按鈕action。。有點僵硬,雖然最后是找到這個方法,但是也不知道怎么進來的,了解mac開發的同學可以指導我一下。

剛說完立馬醒悟了
好吧我再去看了下, 這個應該是系統直接指定的,應該是固定的代理方法。

上傳

接下來按照我了解的某平臺的做法,第一次使用plcrashreporter生成plcrash文件,在第二次啟動的時候進行解析,然后寫為log文件。再進行發送上報的操作。在log內部可以增加標識記錄該log是否已上傳。另外已上傳的可以考慮刪除、當目錄大小超過某個值的時候也可以做刪除操作。這些都是需要自己實現的。

在測試的時候還遇到一個問題
首先我們已經知道Xcode調試模式下無法獲取到crash日志,但是作者在框架內部做了控制,xcode的運行直接崩潰,我嘗試通過作者demo中利用debugger_should_exit()中類似的方式去修改源碼所相關的地方,但還是不奏效。無奈之下只好暫時利用這個函數加以控制crashreporter的開關來保證Xcode的正常調試.


2.10更新:

demo測試中發現得到的crash日志是這樣的:


QQ20170210-0@2x.png

這個和上述提到的有些許不同,不是所謂的地址信息,已經被解析了。可以直接看到出錯的堆棧信息,怎么不需要上述提到的dSYM呢?
發現在啟動的時候有個配置項:

 PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType: PLCrashReporterSignalHandlerTypeMach
                                                                           symbolicationStrategy: PLCrashReporterSymbolicationStrategyAll];

PLCrashReporterSymbolicationStrategyNone = 0,
PLCrashReporterSymbolicationStrategySymbolTable = 1 << 0,
PLCrashReporterSymbolicationStrategyObjC = 1 << 1,
PLCrashReporterSymbolicationStrategyAll = (PLCrashReporterSymbolicationStrategySymbolTable|PLCrashReporterSymbolicationStrategyObjC)

如果設置了PLCrashReporterSymbolicationStrategyNone,那日志信息就會是這樣:

QQ20170210-1@2x.png

應該就是在這里支持了直接符號化的工作,但是在注釋中作者提到這種方式不能保證完全的精確,不建議在release下使用。具體可以去看下這個枚舉的注釋內容。
此外在
CrashReporter symbolication client side in ios
PLCrashReporter - How to symbolicate crash data in-process?中也提到了

But you are right, you should not do this:

  1. It is slow, caused the device to lock up when the crash happens for a few seconds
  1. It requires your app to include symbols which increased the app size by 30-50% (on average)
  2. You won't get line number information for your code.
    You should instead symbolicate the crash reports using the dSYM, e.g. on your Mac.


You can symbolicate the crash report in process, this requires three things:

  1. You should do that on then next app start only.
  1. You need the app symbols to be part of the binary, which increases its size by 30-50%
  2. You will not get line numbers.

The latest version of PLCrashReporter is able to do that, but I would not recommend it and rather symbolicate it using the dSYM because it is way more helpful.

  1. APP的大小30-50%這點,我直接打包倒是沒有發現這個差距。提到了類似需要將app symbols打包進二進制,也沒有研究出這個app symbols具體是什么操作,因為就目前而言,直接不需要多余的配置是可以支持直接符號化的。同時提一下,這也都是4 years ago的問題資料了。
  2. 至于第二次啟動這個點,本身作者在crash時在cache目錄保存了一份日志,啟動后只是做了復制操作。所以在閃退發生后已經保存了符號化后的日志信息。

最終還是推薦使用dSYM自己進行符號化解析,能得到更多的信息,文件名,行號等,也更加的準確。所以在開發和release時可以切換這個選項。


KSCrash

根據github上的commit記錄來看,這個庫的維護頻率要比plcrashreporter高很多,并且有比較詳細的README可以了解相關使用方式,大家可以優先了解這個庫。之所以我先嘗試plcrashreporter的集成是因為我看到某平臺也是使用這種方案的,并且沒有README的介紹,于是就先做下去了。KSCrash的介紹比較詳細,后續會再進行對比。(簡單的跑了下demo,獲取到的日志是一個json文件,并且格式與代碼中拼接中的不一樣,還沒有進一步了解)。

最近訂閱了iOS成長之路3期,主要是今年WWDC中的一些技術點,作為菜鳥們,可以快速的學習和了解。

《iOS成長之路3期·WWDC17內參》
https://xiaozhuanlan.com/wwdc17?rel=5089513982

加個第一期的鏈接,我之前是在作者們的淘寶店買的。

《iOS 成長之路》
https://xiaozhuanlan.com/iosdev?rel=5089513982

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容