前段時(shí)間做了下項(xiàng)目里面的crash監(jiān)測(cè)(自己攔截監(jiān)測(cè)锅睛,不是bugly那些東西)咙好,然后保存crash信息乎澄,同步給服務(wù)器酌泰,進(jìn)行crashlog解析漠畜,這里總結(jié)下相關(guān)的流程和知識(shí)點(diǎn)曹洽。后面會(huì)有完整demo致燥。
一、概念性東西
符號(hào)表
1赂韵、概念:符號(hào)表就是指在Xcode項(xiàng)目編譯后娱节,在編譯生成的.app的同級(jí)目錄下生成的同名的.dSYM文件。
.dSYM文件其實(shí)是一個(gè)目錄祭示,在子目錄中包含了一個(gè)16進(jìn)制的保存函數(shù)地址映射信息的中轉(zhuǎn)文件括堤,所有Debug的symbols都在這個(gè)文件中(包括文件名、函數(shù)名绍移、行號(hào)等)悄窃,所以也稱之為調(diào)試符號(hào)信息文件。
2蹂窖、作用:符號(hào)表就是用來(lái)符號(hào)化 crash log(崩潰日志)轧抗。crash log中有一些方法16進(jìn)制的內(nèi)存地址等,通過(guò)符號(hào)表就能找到對(duì)應(yīng)的能夠直觀看到的方法名之類瞬测。
3横媚、獲取途徑:在Archive的時(shí)候會(huì)生成.xcarchive文件,然后顯示包內(nèi)容就能夠在里面找到.dsYM文件和.app文件月趟。
二灯蝴、Crash捕獲
iOS端的crash分為兩類,一類是NSException異常孝宗,另外一類是Signal信號(hào)異常穷躁。這兩類異常我們都可以通過(guò)注冊(cè)相關(guān)函數(shù)來(lái)捕獲。
1因妇、NSException異常捕獲
NSException異常是OC代碼導(dǎo)致的crash问潭,我們可以先調(diào)用NSGetUncaughtExceptionHandler獲取之前注冊(cè)的handler,如果有就保存起來(lái)婚被,再通過(guò)NSSetUncaughtExceptionHandler方法注冊(cè)自己的handler狡忙。
NSUncaughtExceptionHandler *OldHandler = nil;
void RegisterExceptionHandler(void) {
if (NSGetUncaughtExceptionHandler() != MyExceptionHandler) {
OldHandler = NSGetUncaughtExceptionHandler();
}
NSSetUncaughtExceptionHandler(&MyExceptionHandler);
}
注意:這里需要保存之前注冊(cè)的handler的原因是,很多第三方SDK都會(huì)集成一個(gè)Crash收集服務(wù)址芯,以及時(shí)發(fā)現(xiàn)自己SDK的問(wèn)題灾茁。當(dāng)各家的服務(wù)都以保證自己的Crash統(tǒng)計(jì)正確完整為目的時(shí),難免出現(xiàn)時(shí)序手腳谷炸,強(qiáng)行覆蓋等等的惡意競(jìng)爭(zhēng)北专,總會(huì)有人默默被坑。
如果同時(shí)有多方通過(guò)NSSetUncaughtExceptionHandler注冊(cè)異常處理程序淑廊,和平的作法是:后注冊(cè)者通過(guò)NSGetUncaughtExceptionHandler將先前別人注冊(cè)的handler取出并備份逗余,在自己handler處理完后自覺把別人的handler注冊(cè)回去,規(guī)規(guī)矩矩的傳遞季惩。不傳遞強(qiáng)行覆蓋的后果是录粱,在其之前注冊(cè)過(guò)的日志收集服務(wù)寫出的Crash日志就會(huì)因?yàn)槿〔坏絅SException而丟失Last Exception Backtrace等信息腻格。(P.S. iOS系統(tǒng)自帶的Crash Reporter不受影響)
下面附上MyExceptionHandler
的實(shí)現(xiàn)
NSString * const UncaughtExceptionHandlerAddressesKey = @"UncaughtExceptionHandlerAddressesKey";
//oc exception
void MyExceptionHandler(NSException *exception) {
NSArray *callStack = exception.callStackSymbols;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
[userInfo setObject:callStack forKey:UncaughtExceptionHandlerAddressesKey];
[[[GHCrashManager alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject:[NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo] waitUntilDone:YES];
// 調(diào)用之前已經(jīng)注冊(cè)的handler
if (OldHandler) {
OldHandler(exception);
}
}
在這里就已經(jīng)拿到了異常的NSException *exception
對(duì)象,然后將里面的信息取出來(lái)做本地保存即可啥繁。
2菜职、Signal異常
使用Objective-C的異常處理是不能得到signal的,如果要處理它旗闽,我們還要利用unix標(biāo)準(zhǔn)的signal機(jī)制酬核。
Signal信號(hào)是由iOS底層mach信號(hào)異常轉(zhuǎn)換后以signal信號(hào)拋出的異常。既然是兼容posix標(biāo)準(zhǔn)的異常适室,我們可以通過(guò)sigaction函數(shù)以及signal函數(shù)注冊(cè)對(duì)應(yīng)的信號(hào)嫡意。
這里會(huì)有兩種實(shí)現(xiàn):
第一種:采用signal函數(shù)進(jìn)行信號(hào)捕獲(這里只注冊(cè)了部分常見的信號(hào))
void RegisterSignalHandler(void) {
signal(SIGHUP, SignalHandler);
signal(SIGINT, SignalHandler);
signal(SIGQUIT, SignalHandler);
signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);
}
接下來(lái)附上獲取到信號(hào)后的實(shí)現(xiàn):
void SignalHandler(int signal) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSNumber numberWithInt:signal] forKey:UncaughtExceptionHandlerSignalKey];
NSArray *callBack = [GHCrashManager backtrace];
[userInfo setObject:callBack forKey:UncaughtExceptionHandlerAddressesKey];
NSException *signalException = [NSException exceptionWithName:UncaughtExceptionHandlerSignalExceptionName reason:[NSString stringWithFormat:@"Signal %d was raised.",signal] userInfo:userInfo];
[[[GHCrashManager alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject:signalException waitUntilDone:YES];
}
第二種:采用sigaction函數(shù)進(jìn)行信號(hào)捕獲
void (*OldAbrtSignalHandler)(int, struct __siginfo *, void *);
void RegisterSignalHandler(void) {
struct sigaction old_action;
sigaction(SIGABRT, NULL, &old_action);
if (old_action.sa_flags & SA_SIGINFO) {
if (old_action.sa_sigaction != MySignalHandler) {
OldAbrtSignalHandler = old_action.sa_sigaction;
}
}
struct sigaction action;
action.sa_sigaction = MySignalHandler;
action.sa_flags = SA_NODEFER | SA_SIGINFO;
sigemptyset(&action.sa_mask);
sigaction(SIGABRT, &action, 0);
}
這里同樣做了保存先前別人注冊(cè)的handler。
static void MySignalHandler(int signal, siginfo_t* info, void* context) {
SignalHandler(signal);
// 處理前者注冊(cè)的 handler
if (signal == SIGABRT) {
if (OldAbrtSignalHandler) {
OldAbrtSignalHandler(signal, info, context);
}
}
}
簡(jiǎn)單說(shuō)一下sigaction和signal函數(shù)的區(qū)別:
Linux主要有兩個(gè)函數(shù)實(shí)現(xiàn)信號(hào)的安裝登記:signal和sigaction捣辆。其中signal在系統(tǒng)調(diào)用的基礎(chǔ)上實(shí)現(xiàn)蔬螟,是庫(kù)函數(shù)。它只有兩個(gè)參數(shù)汽畴,不支持信號(hào)傳遞信息旧巾,主要是用于前32個(gè)非實(shí)時(shí)信號(hào)的安裝;而sigaction是較新的函數(shù)(由兩個(gè)系統(tǒng)調(diào)用實(shí)現(xiàn):sys_signal以及sys_rt_sigaction)忍些,有三個(gè)參數(shù)鲁猩,支持信號(hào)傳遞信息,主要用來(lái)與sigqueue系統(tǒng)調(diào)用配合使用罢坝。當(dāng)然廓握,sigaction同樣支持非實(shí)時(shí)信號(hào)的安裝,sigaction優(yōu)于signal主要體現(xiàn)在支持信號(hào)帶有參數(shù)炸客,而signal使用簡(jiǎn)單疾棵,如果沒有復(fù)雜使用場(chǎng)景可以直接使用signal函數(shù)戈钢。
附上大部分信號(hào)的說(shuō)明:
-
SIGHUP
本信號(hào)在用戶終端連接(正潮韵桑或非正常)結(jié)束時(shí)發(fā)出, 通常是在終端的控制進(jìn)程結(jié)束時(shí), 通知同一session內(nèi)的各個(gè)作業(yè), 這時(shí)它們與控制終端不再關(guān)聯(lián)。
登錄Linux時(shí)殉了,系統(tǒng)會(huì)分配給登錄用戶一個(gè)終端(Session)开仰。在這個(gè)終端運(yùn)行的所有程序,包括前臺(tái)進(jìn)程組和后臺(tái)進(jìn)程組薪铜,一般都屬于這個(gè) Session众弓。當(dāng)用戶退出Linux登錄時(shí),前臺(tái)進(jìn)程組和后臺(tái)有對(duì)終端輸出的進(jìn)程將會(huì)收到SIGHUP信號(hào)隔箍。這個(gè)信號(hào)的默認(rèn)操作為終止進(jìn)程谓娃,因此前臺(tái)進(jìn) 程組和后臺(tái)有終端輸出的進(jìn)程就會(huì)中止。不過(guò)可以捕獲這個(gè)信號(hào)蜒滩,比如wget能捕獲SIGHUP信號(hào)滨达,并忽略它奶稠,這樣就算退出了Linux登錄, wget也 能繼續(xù)下載捡遍。
此外锌订,對(duì)于與終端脫離關(guān)系的守護(hù)進(jìn)程,這個(gè)信號(hào)用于通知它重新讀取配置文件画株。 -
SIGINT
程序終止(interrupt)信號(hào), 在用戶鍵入INTR字符(通常是Ctrl-C)時(shí)發(fā)出辆飘,用于通知前臺(tái)進(jìn)程組終止進(jìn)程。 -
SIGQUIT
和SIGINT類似, 但由QUIT字符(通常是Ctrl-)來(lái)控制. 進(jìn)程在因收到SIGQUIT退出時(shí)會(huì)產(chǎn)生core文件, 在這個(gè)意義上類似于一個(gè)程序錯(cuò)誤信號(hào)谓传。 -
SIGILL
執(zhí)行了非法指令. 通常是因?yàn)榭蓤?zhí)行文件本身出現(xiàn)錯(cuò)誤, 或者試圖執(zhí)行數(shù)據(jù)段. 堆棧溢出時(shí)也有可能產(chǎn)生這個(gè)信號(hào)蜈项。 -
SIGTRAP
由斷點(diǎn)指令或其它trap指令產(chǎn)生. 由debugger使用。 -
SIGABRT
調(diào)用abort函數(shù)生成的信號(hào)续挟。 -
SIGBUS
非法地址, 包括內(nèi)存地址對(duì)齊(alignment)出錯(cuò)战得。比如訪問(wèn)一個(gè)四個(gè)字長(zhǎng)的整數(shù), 但其地址不是4的倍數(shù)。它與SIGSEGV的區(qū)別在于后者是由于對(duì)合法存儲(chǔ)地址的非法訪問(wèn)觸發(fā)的(如訪問(wèn)不屬于自己存儲(chǔ)空間或只讀存儲(chǔ)空間)庸推。 -
SIGFPE
在發(fā)生致命的算術(shù)運(yùn)算錯(cuò)誤時(shí)發(fā)出. 不僅包括浮點(diǎn)運(yùn)算錯(cuò)誤, 還包括溢出及除數(shù)為0等其它所有的算術(shù)的錯(cuò)誤常侦。 -
SIGKILL
用來(lái)立即結(jié)束程序的運(yùn)行. 本信號(hào)不能被阻塞、處理和忽略贬媒。如果管理員發(fā)現(xiàn)某個(gè)進(jìn)程終止不了聋亡,可嘗試發(fā)送這個(gè)信號(hào)。 -
SIGUSR1
留給用戶使用 -
SIGSEGV
試圖訪問(wèn)未分配給自己的內(nèi)存, 或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù). -
SIGUSR2
留給用戶使用 -
SIGPIPE
管道破裂际乘。這個(gè)信號(hào)通常在進(jìn)程間通信產(chǎn)生坡倔,比如采用FIFO(管道)通信的兩個(gè)進(jìn)程,讀管道沒打開或者意外終止就往管道寫脖含,寫進(jìn)程會(huì)收到SIGPIPE信號(hào)罪塔。此外用Socket通信的兩個(gè)進(jìn)程,寫進(jìn)程在寫Socket的時(shí)候养葵,讀進(jìn)程已經(jīng)終止征堪。 -
SIGALRM
時(shí)鐘定時(shí)信號(hào), 計(jì)算的是實(shí)際的時(shí)間或時(shí)鐘時(shí)間. alarm函數(shù)使用該信號(hào). -
SIGTERM
程序結(jié)束(terminate)信號(hào), 與SIGKILL不同的是該信號(hào)可以被阻塞和處理。通常用來(lái)要求程序自己正常退出关拒,shell命令kill缺省產(chǎn)生這個(gè)信號(hào)佃蚜。如果進(jìn)程終止不了,我們才會(huì)嘗試SIGKILL着绊。 -
SIGCHLD
子進(jìn)程結(jié)束時(shí), 父進(jìn)程會(huì)收到這個(gè)信號(hào)谐算。
如果父進(jìn)程沒有處理這個(gè)信號(hào),也沒有等待(wait)子進(jìn)程归露,子進(jìn)程雖然終止洲脂,但是還會(huì)在內(nèi)核進(jìn)程表中占有表項(xiàng),這時(shí)的子進(jìn)程稱為僵尸進(jìn)程剧包。這種情 況我們應(yīng)該避免(父進(jìn)程或者忽略SIGCHILD信號(hào)恐锦,或者捕捉它雇毫,或者wait它派生的子進(jìn)程,或者父進(jìn)程先終止踩蔚,這時(shí)子進(jìn)程的終止自動(dòng)由init進(jìn)程 來(lái)接管)棚放。 -
SIGCONT
讓一個(gè)停止(stopped)的進(jìn)程繼續(xù)執(zhí)行. 本信號(hào)不能被阻塞. 可以用一個(gè)handler來(lái)讓程序在由stopped狀態(tài)變?yōu)槔^續(xù)執(zhí)行時(shí)完成特定的工作. 例如, 重新顯示提示符 -
SIGSTOP
停止(stopped)進(jìn)程的執(zhí)行. 注意它和terminate以及interrupt的區(qū)別:該進(jìn)程還未結(jié)束, 只是暫停執(zhí)行. 本信號(hào)不能被阻塞, 處理或忽略. -
SIGTSTP
停止進(jìn)程的運(yùn)行, 但該信號(hào)可以被處理和忽略. 用戶鍵入SUSP字符時(shí)(通常是Ctrl-Z)發(fā)出這個(gè)信號(hào) -
SIGTTIN
當(dāng)后臺(tái)作業(yè)要從用戶終端讀數(shù)據(jù)時(shí), 該作業(yè)中的所有進(jìn)程會(huì)收到SIGTTIN信號(hào). 缺省時(shí)這些進(jìn)程會(huì)停止執(zhí)行. -
SIGTTOU
類似于SIGTTIN, 但在寫終端(或修改終端模式)時(shí)收到. -
SIGURG
有”緊急”數(shù)據(jù)或out-of-band數(shù)據(jù)到達(dá)socket時(shí)產(chǎn)生. -
SIGXCPU
超過(guò)CPU時(shí)間資源限制. 這個(gè)限制可以由getrlimit/setrlimit來(lái)讀取/改變。 -
SIGXFSZ
當(dāng)進(jìn)程企圖擴(kuò)大文件以至于超過(guò)文件大小資源限制馅闽。 -
SIGVTALRM
虛擬時(shí)鐘信號(hào). 類似于SIGALRM, 但是計(jì)算的是該進(jìn)程占用的CPU時(shí)間. -
SIGPROF
類似于SIGALRM/SIGVTALRM, 但包括該進(jìn)程用的CPU時(shí)間以及系統(tǒng)調(diào)用的時(shí)間. -
SIGWINCH
窗口大小改變時(shí)發(fā)出. -
SIGIO
文件描述符準(zhǔn)備就緒, 可以開始進(jìn)行輸入/輸出操作. -
SIGPWR
Power failure -
SIGSYS
非法的系統(tǒng)調(diào)用飘蚯。
其中要注意:
- 在以上列出的信號(hào)中,程序不可捕獲福也、阻塞或忽略的信號(hào)有:SIGKILL,SIGSTOP
- 不能恢復(fù)至默認(rèn)動(dòng)作的信號(hào)有:SIGILL,SIGTRAP
- 默認(rèn)會(huì)導(dǎo)致進(jìn)程流產(chǎn)的信號(hào)有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默認(rèn)會(huì)導(dǎo)致進(jìn)程退出的信號(hào)有: - SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
- 默認(rèn)會(huì)導(dǎo)致進(jìn)程停止的信號(hào)有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
- 默認(rèn)進(jìn)程忽略的信號(hào)有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
- 此外局骤,SIGIO在SVR4是退出,在4.3BSD中是忽略暴凑;SIGCONT在進(jìn)程掛起時(shí)是繼續(xù)峦甩,否則是忽略,不能被阻塞现喳。
另:
在debug模式下凯傲,如果你觸發(fā)了signal崩潰,那么應(yīng)用會(huì)直接崩潰到主函數(shù)嗦篱,斷點(diǎn)都沒用冰单,此時(shí)沒有任何log信息顯示出來(lái),如果你想看log信息的話灸促,你需要在lldb中诫欠,拿SIGABRT來(lái)說(shuō)吧,敲入pro hand -p true -s false SIGABRT
命令浴栽,不然你啥也看不到荒叼。或者也可以不連接xcode去run典鸡,如果你照著后面的crash捕獲后處理了的話被廓。
三、Crash捕獲之后的處理
拿到exception對(duì)象后椿每,做三件事:
1伊者、如果是debug環(huán)境下提示使用者;
2间护、將crash信息保存在本地;
3挖诸、移除自己的注冊(cè)操作汁尺;
(當(dāng)然還有4、下次啟動(dòng)上傳crashLog)多律。
1痴突、show出異常信息
貼視圖就不用說(shuō)了搂蜓,重點(diǎn)是要防止當(dāng)前線程掛掉,那么就得使用runloop了辽装。
#ifdef DEBUG
NSString *message = [NSString stringWithFormat:@"抱歉帮碰,APP發(fā)生了異常,請(qǐng)與開發(fā)人員聯(lián)系拾积,點(diǎn)擊屏幕繼續(xù)并自動(dòng)復(fù)制錯(cuò)誤信息到剪切板殉挽。\n\n異常報(bào)告:\n異常名稱:%@\n異常原因:%@\n堆棧信息:%@\n", [exception name], [exception reason], stackInfo];
NSLog(@"%@",message);
[self showCrashToastWithMessage:message];//貼視圖去顯示,建議直接丟到window上
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!self.dismissed) {
for (NSString *mode in (__bridge NSArray *)allModes) {
//為阻止線程退出拓巧,使用 CFRunLoopRunInMode(model, 0.001, false)等待系統(tǒng)消息斯碌,false表示RunLoop沒有超時(shí)時(shí)間
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
#endif
2、存儲(chǔ)就不說(shuō)了肛度,讀寫文件的事傻唾。
3、移除自己的注冊(cè)
NSSetUncaughtExceptionHandler(NULL);
signal(SIGHUP, SIG_DFL);
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
NSLog(@"%@",[exception name]);
if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName]) {
kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
} else {
[exception raise];
}
到這里crash的捕獲和本地存儲(chǔ)都已經(jīng)ok了承耿,log也能上傳到服務(wù)器了冠骄,接下來(lái)就剩拿到log和dsym文件進(jìn)行符號(hào)化了。
如果你想了解crash的原理和根源加袋,建議讀讀漫談iOS Crash收集框架猴抹。
另外,
demo在這里锁荔,里面也附上了幾種常見crash的案例蟀给,結(jié)合案例測(cè)試下吧。