本文整理下最近對于crash采集的總結(jié),和踩過的坑。
CrashReporter
首先,iOS有自己的CrashReporter機(jī)制。在真機(jī)上產(chǎn)生的crash,在一下兩個地方可以找到:
-
Xcode-Window-Devices - View Device Logs中可以看到crash文件。這是我的截圖:
QQ20170125-0@2x.png
關(guān)于各個字段的含義,我搜集了相關(guān)博客的介紹,有不對的地方大家可以指出:
字段 | 含義 |
---|---|
Incident Identifier | 當(dāng)前crash的 id,可以區(qū)分不同的crash事件 |
CrashReporter Key | 當(dāng)前設(shè)備的id,可以判斷crash在某一設(shè)備上出現(xiàn)的頻率 |
Hardware Model | 設(shè)備型號 |
Process | 當(dāng)前應(yīng)用的名稱,后面中括號中為當(dāng)前的應(yīng)用在系統(tǒng)中的進(jìn)程id |
Path | 當(dāng)前應(yīng)用在設(shè)備中的路徑 |
Identifier | bundle id |
Version | 應(yīng)用版本號 |
Code Type | 還不清楚 |
Date/Time | crash事件 時間(后面跟的應(yīng)該是時區(qū)) |
OS Version | 當(dāng)前系統(tǒng)版本 |
Exception Type | 異常類型 |
Exception Codes | 異常出錯的代碼(常見代碼有以下幾種) 0x8badf00d錯誤碼:Watchdog超時,意為“ate bad food”。 0xdeadfa11錯誤碼:用戶強(qiáng)制退出,意為“dead fall”。 0xbaaaaaad錯誤碼:用戶按住Home鍵和音量鍵,獲取當(dāng)前內(nèi)存狀態(tài),不代表崩潰。 0xbad22222錯誤碼:VoIP應(yīng)用(因為太頻繁?)被iOS干掉。 0xc00010ff錯誤碼:因為太燙了被干掉,意為“cool off”。 0xdead10cc錯誤碼:因為在后臺時仍然占據(jù)系統(tǒng)資源(比如通訊錄)被干掉,意為“dead lock”。 |
Triggered by Thread | 在某一個線程出了問題導(dǎo)致crash,Thread 0 為主線程、其它的都為子線程 |
Last Exception Backtrace | 最后異常回溯,一般根據(jù)這個代碼就能找到crash的具體問題 |
-
通過iTunes Connect(Manage Your Applications - View Details - Crash Reports)獲取用戶的crash日志。需要用戶在設(shè)置-診斷與用量中允許將崩潰信息發(fā)送給開發(fā)者。然后在也可以在Xcode的Window - Organizer中可以看到對應(yīng)的crash信息。(需要在Xcode中登錄所屬的開發(fā)者賬號)
QQ20170125-1@2x.png
.dSYM文件
這是你拿到的crash日志:
取到的crash文件的崩潰信息會是地址信息,這時候需要使用打包時對應(yīng)的dSYM文件進(jìn)行符號表的解析工作,所以每次生產(chǎn)版本打包時,都需要保存對應(yīng)的dSYM文件,一些第三方的crash采集分析平臺也會要求上傳對應(yīng)的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文件相同的目錄中。
進(jìn)入所在目錄
cd /Users/username/Desktop/CrashReport
依次執(zhí)行以下的命令即可輸出為目標(biāo)文件symbol.crash
export DEVELOPER_DIR=/Applications/XCode.app/Contents/Developer
./symbolicatecrash ./*.crash ./*.app.dSYM > symbol.crash
以上這種獲取crash信息的方式不夠滿足我們產(chǎn)品的需要,想通過用戶主動上傳或者同意發(fā)送崩潰信息存在太多的困難。
依靠程序?qū)崿F(xiàn)crash的捕捉
在搜索相關(guān)資料的時候,比較常見的方式分兩種。
異常處理機(jī)制
同時對于系統(tǒng)Crash而引起的程序異常退出,可以通過UncaughtExceptionHandler
機(jī)制捕獲;
也就是說在程序中catch以外的內(nèi)容,被系統(tǒng)自帶的錯誤處理而捕獲。我們要做的就是用自定義的函數(shù)替代該ExceptionHandler即可。
這里主要有兩個函數(shù)
NSGetUncaughtExceptionHandler() 得到現(xiàn)在系統(tǒng)自帶處理Handler;得到它后,如果程序正常退出時用來回復(fù)系統(tǒng)原先設(shè)置
NSSetUncaughtExceptionHandler() 紅色設(shè)置自定義的函數(shù)
該方式可以捕捉到常見的數(shù)組越界等OC層面拋出的異常。
PS:在設(shè)置handler時需要注意一點。在念茜的《漫談iOS Crash收集框架》中提到
如果同時有多方通過NSSetUncaughtExceptionHandler注冊異常處理程序,和平的作法是:后注冊者通過NSGetUncaughtExceptionHandler將先前別人注冊的handler取出并備份,在自己handler處理完后自覺把別人的handler注冊回去,規(guī)規(guī)矩矩的傳遞。不傳遞強(qiáng)行覆蓋的后果是,在其之前注冊過的日志收集服務(wù)寫出的Crash日志就會因為取不到NSException而丟失Last Exception Backtrace等信息。(P.S. iOS系統(tǒng)自帶的Crash Reporter不受影響)
建議在自己的handle處理完之后,設(shè)置回原先保存的別人注冊的handler
處理signal
除了OC層面的異常捕捉之外,很多內(nèi)存錯誤、訪問錯誤的地址產(chǎn)生的crash則需要利用unix標(biāo)準(zhǔn)的signal機(jī)制,注冊SIGABRT, SIGBUS, SIGSEGV等信號發(fā)生時的處理函數(shù)。該函數(shù)中我們可以輸出棧信息,版本信息等其他一切我們所想要的。
實例代碼:
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);
}
關(guān)于這塊,雖說能找到很多類似的、相互轉(zhuǎn)載的資料,但是大部分的代碼都多多少少有問題,沒有奏效。放個最后找到的可以用的地址。
關(guān)于上述提到的多方通過NSSetUncaughtExceptionHandler注冊異常時候的處理,所以我把這步優(yōu)化加上了。我的demo
ps:關(guān)于signal信號的捕捉,在Xcode調(diào)試時,Debugger模式會先于我們的代碼catch到所有的crash,所以需要直接從模擬器中進(jìn)入程序才可以測試
相關(guān)開源庫的實現(xiàn)
至此,簡單的crash采集工作基本算是完成了,能一定程度上滿足對于crash日志信息采集的需求了,也能從信息中定位到問題所在。
但是這種方式獲取到的日志信息(指signal信號捕捉的信息)有簡單的崩潰堆棧信息,不需要進(jìn)行符號表的反解。
并且我查看了某個平臺的crash文件格式,上文說到平臺需要提前上傳dSYM文件。文件格式和系統(tǒng)生成的crash文件基本一致,該有的字段信息都有。所以相關(guān)實現(xiàn)肯定是不一樣的,在翻閱頭文件的時候看到了#import <mach/mach.h>
,回想起上文提到的念茜去年的一篇博客 -《漫談iOS Crash收集框架》。之前看的時候,云里霧里,現(xiàn)在稍許有些概念。
所有Mach異常都在host層被ux_exception轉(zhuǎn)換為相應(yīng)的Unix信號,并通過threadsignal將信號投遞到出錯的線程。iOS中的 POSIX API 就是通過 Mach 之上的 BSD 層實現(xiàn)的。
因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach層的EXC_BAD_ACCESS異常,在host層被轉(zhuǎn)換成SIGSEGV信號投遞到出錯的線程。既然最終以信號的方式投遞到出錯的線程,那么就可以通過注冊signalHandler來捕獲信號:
signal(SIGSEGV,signalHandler);
捕獲Mach異常或者Unix信號都可以抓到crash事件,這兩種方式哪個更好呢?
優(yōu)選Mach異常,因為Mach異常處理會先于Unix信號處理發(fā)生,如果Mach異常的handler讓程序exit了,那么Unix信號就永遠(yuǎn)不會到達(dá)這個進(jìn)程了。轉(zhuǎn)換Unix信號是為了兼容更為流行的POSIX標(biāo)準(zhǔn)(SUS規(guī)范),這樣不必了解Mach內(nèi)核也可以通過Unix信號的方式來兼容開發(fā)。
猜測就是通過mach的相關(guān)接口獲取到崩潰信息的。于是去github上找了相關(guān)的開源KSCrash,plcrashreporter。確實這兩個庫中都得到了對應(yīng)上述的crash文件中大部分的信息。于是開始著手plcrashreporter的集成使用。
plcrashreporter
集成
作者在工程里新建了多個target,對應(yīng)模擬器的.a庫、iOS的.a庫、iOS的framework、Mac的framework等。對framework也做了模擬器和真機(jī)版本的合并操作。直接將對應(yīng)的framework拖入到自己工程中使用就可以了。
相關(guān)的集成代碼包括:
// 是的調(diào)試模式下是無法獲取到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格式到本地,進(jìn)行自己的上報操作。在工具中可以看到主要的解析代碼是:
- (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按鈕找到對應(yīng)的處理函數(shù),但是竟然找不到對應(yīng)的按鈕action。。有點僵硬,雖然最后是找到這個方法,但是也不知道怎么進(jìn)來的,了解mac開發(fā)的同學(xué)可以指導(dǎo)我一下。
剛說完立馬醒悟了
好吧我再去看了下, 這個應(yīng)該是系統(tǒng)直接指定的,應(yīng)該是固定的代理方法。
上傳
接下來按照我了解的某平臺的做法,第一次使用plcrashreporter生成plcrash文件,在第二次啟動的時候進(jìn)行解析,然后寫為log文件。再進(jìn)行發(fā)送上報的操作。在log內(nèi)部可以增加標(biāo)識記錄該log是否已上傳。另外已上傳的可以考慮刪除、當(dāng)目錄大小超過某個值的時候也可以做刪除操作。這些都是需要自己實現(xiàn)的。
在測試的時候還遇到一個問題
首先我們已經(jīng)知道Xcode調(diào)試模式下無法獲取到crash日志,但是作者在框架內(nèi)部做了控制,xcode的運(yùn)行直接崩潰,我嘗試通過作者demo中利用debugger_should_exit()
中類似的方式去修改源碼所相關(guān)的地方,但還是不奏效。無奈之下只好暫時利用這個函數(shù)加以控制crashreporter的開關(guān)來保證Xcode的正常調(diào)試.
2.10更新:
demo測試中發(fā)現(xiàn)得到的crash日志是這樣的:
這個和上述提到的有些許不同,不是所謂的地址信息,已經(jīng)被解析了。可以直接看到出錯的堆棧信息,怎么不需要上述提到的dSYM呢?
發(fā)現(xiàn)在啟動的時候有個配置項:
PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType: PLCrashReporterSignalHandlerTypeMach
symbolicationStrategy: PLCrashReporterSymbolicationStrategyAll];
PLCrashReporterSymbolicationStrategyNone = 0,
PLCrashReporterSymbolicationStrategySymbolTable = 1 << 0,
PLCrashReporterSymbolicationStrategyObjC = 1 << 1,
PLCrashReporterSymbolicationStrategyAll = (PLCrashReporterSymbolicationStrategySymbolTable|PLCrashReporterSymbolicationStrategyObjC)
如果設(shè)置了PLCrashReporterSymbolicationStrategyNone
,那日志信息就會是這樣:
應(yīng)該就是在這里支持了直接符號化的工作,但是在注釋中作者提到這種方式不能保證完全的精確,不建議在release下使用。具體可以去看下這個枚舉的注釋內(nèi)容。
此外在
CrashReporter symbolication client side in ios
PLCrashReporter - How to symbolicate crash data in-process?中也提到了
But you are right, you should not do this:
- It is slow, caused the device to lock up when the crash happens for a few seconds
- It requires your app to include symbols which increased the app size by 30-50% (on average)
- 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:
- You should do that on then next app start only.
- You need the app symbols to be part of the binary, which increases its size by 30-50%
- 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.
- APP的大小30-50%這點,我直接打包倒是沒有發(fā)現(xiàn)這個差距。提到了類似需要將app symbols打包進(jìn)二進(jìn)制,也沒有研究出這個app symbols具體是什么操作,因為就目前而言,直接不需要多余的配置是可以支持直接符號化的。同時提一下,這也都是4 years ago的問題資料了。
- 至于第二次啟動這個點,本身作者在crash時在cache目錄保存了一份日志,啟動后只是做了復(fù)制操作。所以在閃退發(fā)生后已經(jīng)保存了符號化后的日志信息。
最終還是推薦使用dSYM自己進(jìn)行符號化解析,能得到更多的信息,文件名,行號等,也更加的準(zhǔn)確。所以在開發(fā)和release時可以切換這個選項。
KSCrash
根據(jù)github上的commit記錄來看,這個庫的維護(hù)頻率要比plcrashreporter高很多,并且有比較詳細(xì)的README可以了解相關(guān)使用方式,大家可以優(yōu)先了解這個庫。之所以我先嘗試plcrashreporter的集成是因為我看到某平臺也是使用這種方案的,并且沒有README的介紹,于是就先做下去了。KSCrash的介紹比較詳細(xì),后續(xù)會再進(jìn)行對比。(簡單的跑了下demo,獲取到的日志是一個json文件,并且格式與代碼中拼接中的不一樣,還沒有進(jìn)一步了解)。
最近訂閱了iOS成長之路3期,主要是今年WWDC中的一些技術(shù)點,作為菜鳥們,可以快速的學(xué)習(xí)和了解。
《iOS成長之路3期·WWDC17內(nèi)參》
https://xiaozhuanlan.com/wwdc17?rel=5089513982