盡管當iphone應(yīng)用崩潰時,它不會告訴用戶發(fā)生了什么,但我們?nèi)匀豢梢詾閼?yīng)用添加異常和信號處理,以此記錄和展示發(fā)生的變化。更進一步,我們甚至能在異常發(fā)生時,阻止應(yīng)用的崩潰。
引言
本文所用應(yīng)用將拋出指定的Object-C異常,如EXC_BAD_ACCESS異常和響應(yīng)的BSD signal。經(jīng)過處理后,所有的異常和信號都會被捕獲,然后展示調(diào)試信息,最后應(yīng)用還能繼續(xù)運行。

上圖應(yīng)用會在4秒后故意觸發(fā)一個未處理消息異常,并在10秒后觸發(fā)
EXC_BAD_ACCESS/SIGBUS信號。
iPhone應(yīng)用崩潰原因
崩潰(準確的說是程序異常終止)是程序接收到未處理信號的結(jié)果。
未處理信號有三個來源:內(nèi)核、其他進程和應(yīng)用本身。導致崩潰最常見的兩個信號如下:
-
EXC_BAD_ACCESS是一種由內(nèi)核發(fā)出的Mach異常,通常是因為應(yīng)用試圖訪問不存在的內(nèi)存空間導致的。如果未能在Mach內(nèi)核級別進行處理,它將被轉(zhuǎn)化為SIGBUS或者SIGSEGVBSD信號。 -
SIGABRT是當產(chǎn)生未捕獲的NSException或者obj_exception_throw時,應(yīng)用發(fā)給自身的BSD信號。
在Object-C異常中,導致異常拋出最常見的原因是應(yīng)用向?qū)ο蟀l(fā)送了未實現(xiàn)的方法選擇器(比如拼寫錯誤,對象混淆或者向已經(jīng)釋放的對象發(fā)送消息)。
捕獲未捕獲異常(uncaught exceptions)
正確處理未捕獲異常的方式是在代碼中修復異常產(chǎn)生的原因。如果你的程序工作良好,那么下面講述的方法也就不重要了。
當然,有些bug也不一定總會導致應(yīng)用崩潰。此外,當你的程序中出現(xiàn)bug時,你會希望測試人員能夠返回一些有用的信息。
在這些情況下,有兩種方式可以捕獲那些會導致崩潰的未捕獲狀態(tài)。
- 使用
NSUncaughtExceptionHandler函數(shù)來安裝未捕獲Object-C異常的處理器。 - 使用
signal函數(shù)來安裝BSD信號處理器。
例如,安裝Object-C異常處理器和信號處理的代碼如下:
objc
void InstallUncaughtExceptionHandler()
{
NSSetUncaughtExceptionHandler(&HandleException);
signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);
}
objc
對于異常和信號的響應(yīng)會在HandleException和SignalHandler中實現(xiàn)。在樣例程序中,以上二者的處理方式相同。
盡管本文只處理最常見的信號,但是,你可以為自己的程序添加所需的所有異常信號。
注意,有兩種異常是不能捕獲的:SIGKILL和SIGSTOP。它們會終止或者暫停應(yīng)用。(SIGKILL是命令行函數(shù)kill -9發(fā)出的,SIGSTOP是鍵入Control-Z發(fā)出的)。
異常處理的要求
未處理異常處理器可能永遠不能返回
導致未處理異?;蛐盘柼幚砥鞅挥|發(fā)的場景通常都是應(yīng)用中不可逆的場景。
然而,有時僅是棧幀或者當前函數(shù)不能恢復。如果你能阻止當前棧幀繼續(xù)執(zhí)行,那么剩下的程序還可能繼續(xù)執(zhí)行。
如果你想嘗試這種情況,那么你的未處理異常處理器必須不將控制權(quán)返回給正在調(diào)用的函數(shù)——產(chǎn)生異?;蛘哂|發(fā)信號的代碼不允許被再次使用。
為了在不返回控制權(quán)給調(diào)用函數(shù)的情況下,繼續(xù)執(zhí)行代碼,我們必須返回主線程(假設(shè)我們現(xiàn)在不在主線程),并永久阻塞舊線程。同時,在主線程中,我們必須開啟新的run loop,且不在返回原來的run loop。
這意味著被導致異常的線程所使用的棧內(nèi)存將永遠泄漏。這就是代價。
嘗試恢復
由于新的run loop將用來顯示會話,它將保持無限運行,同時,它還可以取代應(yīng)用的主run loop。
為此,該run loop必須能夠處理主線程的所有模式。由于主run loop包含了許多私有模式(如GSEvent處理和滑動跟蹤),默認的NSDefaultRunLoopMode是不夠的。
幸運的是,如果UIApplication已經(jīng)創(chuàng)造了主loop的所有模式,那么,就可以從該loop中讀取所有這些模式。假設(shè)這些代碼在main loop創(chuàng)建后運行在主線程,那么它也能在所有的UIApplication模式下運行循環(huán):
objc
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!dismissed)
{
for (NSString *mode in (NSArray *)allModes)
{
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
objc
作為調(diào)試信息的一部分,我們需要獲取棧地址
你可以使用backtrace函數(shù)來取得回溯信息,并使用backtrace_symbols來將它們轉(zhuǎn)化為標記。
objc
-
(NSArray )backtrace
{
void callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for (
i = UncaughtExceptionHandlerSkipAddressCount;
i < UncaughtExceptionHandlerSkipAddressCount + UncaughtExceptionHandlerReportAddressCount;
i++)
{
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}
objc
注意,我們跳過了開始的一些地址:這是因為它們是信號和異常處理函數(shù)的地址(不重要)。由于我們想要保存最少的數(shù)據(jù)(用于在UIAlert對話框顯示),我選擇放棄顯示異常處理函數(shù)。
如果用戶選擇“退出”,我們將會記錄崩潰日志
如果用戶選擇“退出”來終止應(yīng)用,而不是繼續(xù)運行程序,用常用的崩潰日志處理來記錄記錄問題不失為一個好方法。
在本例中,我們將移除所有的異常處理器,并再次生成異?;蛘咧匦掳l(fā)送信號來使得程序像往常一樣崩潰(盡管未處理異常處理器會出現(xiàn)在棧的頂部,但后面的幀是相同的)。