Crash我們不得不面對的問題,但是好多人在遇到Crash的時候都無從下手,很多的時候都是憑著感覺找問題。今天我做了5篇文章來幫助我們更加清晰的認清iOS中的Crash
想要了解更詳細的內(nèi)容可以點擊這里
Crash分類
一般是由 Mach異?;?Objective-C 異常(NSException)引起的。我們可以針對這兩種情況抓取對應(yīng)的 Crash 事件

- 1、Mach異常是最底層的內(nèi)核級異常,如EXC_BAD_ACCESS(內(nèi)存訪問異常)
- 2、Unix Signal是Unix系統(tǒng)中的一種異步通知機制,Mach異常在host層被ux_exception轉(zhuǎn)換為相應(yīng)的Unix Signal,并通過threadsignal將信號投遞到出錯的線程
- 3、 NSException是OC層,由iOS庫或者各種第三方庫或Runtime驗證出錯誤而拋出的異常。如NSRangeException(數(shù)組越界異常)
- 4、當(dāng)錯誤發(fā)生時候,先在最底層產(chǎn)生Mach異常;Mach異常在host層被轉(zhuǎn)換為相應(yīng)的Unix Signal; 在OC層如果有對應(yīng)的NSException(OC異常),就轉(zhuǎn)換成OC異常,OC異??梢栽贠C層得到處理;如果OC異常一直得不到處理,程序會強行發(fā)送SIGABRT信號中斷程序。在OC層如果沒有對應(yīng)的NSException,就只能讓Unix標(biāo)準(zhǔn)的signal機制來處理了。
- 5、在捕獲Crash事件時,優(yōu)選Mach異常。因為Mach異常處理會先于Unix信號處理發(fā)生,如果Mach異常的handler讓程序exit了,那么Unix信號就永遠不會到達這個進程了。而轉(zhuǎn)換Unix信號是為了兼容更為流行的POSIX標(biāo)準(zhǔn)(SUS規(guī)范),這樣就不必了解Mach內(nèi)核也可以通過Unix信號的方式來兼容開發(fā)
Mach異常
Mach操作系統(tǒng)微內(nèi)核,是許多新操作系統(tǒng)的設(shè)計基礎(chǔ)。Mach微內(nèi)核中有幾個基礎(chǔ)概念:
- Tasks,擁有一組系統(tǒng)資源的對象,允許"thread"在其中執(zhí)行。
- Threads,執(zhí)行的基本單位,擁有task的上下文,并共享其資源。
- Ports,task之間通訊的一組受保護的消息隊列;task可對任何port發(fā)送/接收數(shù)據(jù)。
- Message,有類型的數(shù)據(jù)對象集合,只可以發(fā)送到port。
Mach 異常是指最底層的內(nèi)核級異常,被定義在 <mach/exception_types.h>下。mach異常由處理器陷阱引發(fā),在異常發(fā)生后會被異常處理程序轉(zhuǎn)換成Mach消息,接著依次投遞到thread、task和host端口。如果沒有一個端口處理這個異常并返回KERN_SUCCESS,那么應(yīng)用將被終止。每個端口擁有一個異常端口數(shù)組,系統(tǒng)暴露了后綴為_set_exception_ports的多個API讓我們注冊對應(yīng)的異常處理到端口中
Mach異常方式

Mach提供少量API
// 內(nèi)核中創(chuàng)建一個消息隊列,獲取對應(yīng)的port
mach_port_allocate();
// 授予task對port的指定權(quán)限
mach_port_insert_right();
// 通過設(shè)定參數(shù):MACH_RSV_MSG/MACH_SEND_MSG用于接收/發(fā)送mach message
mach_msg();
Mach異常捕獲
task_set_exception_ports(),設(shè)置內(nèi)核接收Mach異常消息的Port,替換為自定義的Port后,即可捕獲程序執(zhí)行過程中產(chǎn)生的異常消息。
+ (void)createAndSetExceptionPort {
mach_port_t server_port;
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port);
assert(kr == KERN_SUCCESS);
NSLog(@"create a port: %d", server_port);
kr = mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);
kr = task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS | EXC_MASK_CRASH, server_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE);
[self setMachPortListener:server_port];
}
// 構(gòu)造BAD MEM ACCESS Crash
- (void)makeCrash {
NSLog(@"********** Make a [BAD MEM ACCESS] now. **********");
*((int *)(0x1234)) = 122;
}
以上代碼參考iOS Mach異常和signal信號
mach異常即便注冊了對應(yīng)的處理,也不會導(dǎo)致影響原有的投遞流程。此外,即便不去注冊mach異常的處理,最終經(jīng)過一系列的處理,mach異常會被轉(zhuǎn)換成對應(yīng)的UNIX信號,一種mach異常對應(yīng)了一個或者多個信號類型。因此在捕獲crash要提防二次采集的可能。

處理signal
當(dāng)錯誤發(fā)生時候,先在最底層產(chǎn)生Mach異常;Mach異常在host層被轉(zhuǎn)換為相應(yīng)的Unix Signal; 在OC層如果有對應(yīng)的NSException(OC異常),就轉(zhuǎn)換成OC異常,OC異??梢栽贠C層得到處理;如果OC異常一直得不到處理,程序會強行發(fā)送SIGABRT信號中斷程序。在OC層如果沒有對應(yīng)的NSException,就只能讓Unix標(biāo)準(zhǔn)的signal機制來處理了
在signal.h中聲明了32種異常信號,常見的有以下幾種
- 1、SIGILL 執(zhí)行了非法指令,一般是可執(zhí)行文件出現(xiàn)了錯誤
- 2、SIGTRAP 斷點指令或者其他trap指令產(chǎn)生
- 3、SIGABRT 調(diào)用abort產(chǎn)生
- 4、SIGBUS 非法地址。比如錯誤的內(nèi)存類型訪問、內(nèi)存地址對齊等
- 5、SIGSEGV 非法地址。訪問未分配內(nèi)存、寫入沒有寫權(quán)限的內(nèi)存等
- 6、SIGFPE 致命的算術(shù)運算。比如數(shù)值溢出、NaN數(shù)值等
應(yīng)用
1.AppDelegate.m中
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
InstallSignalHandler();//信號量截斷
InstallUncaughtExceptionHandler();//系統(tǒng)異常捕獲
return YES;
}
2.SignalHandler.m的實現(xiàn)
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]];
}
[SignalHandler 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)錯誤類型可以看上面的說明,SignalExceptionHandler是信號出錯時候的回調(diào)。當(dāng)有信號出錯的時候,可以回調(diào)到這個方法
3.UncaughtExceptionHandler.m的實現(xiàn)
void HandleException(NSException *exception)
{
// 異常的堆棧信息
NSArray *stackArray = [exception callStackSymbols];
// 出現(xiàn)異常的原因
NSString *reason = [exception reason];
// 異常名稱
NSString *name = [exception name];
NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
NSLog(@"%@", exceptionInfo);
[UncaughtExceptionHandler saveCreash:exceptionInfo];
}
void InstallUncaughtExceptionHandler(void)
{
NSSetUncaughtExceptionHandler(&HandleException);
}
NSException異常
常見的NSException異常有
- 1、unrecognized selector crash
- 2、KVO crash
- 3、NSNotification crash
- 4、NSTimer crash
- 5、Container crash(數(shù)組越界,插nil等)
- 6、NSString crash (字符串操作的crash)
- 7、Bad Access crash (野指針)
- 8、UI not on Main Thread Crash (非主線程刷UI(機制待改善))
更加詳細的信息請參考Baymax:網(wǎng)易iOS App運行時Crash自動防護實踐
unrecognized selector類型
unrecognized selector類型的crash在app眾多的crash類型中占著比較大的成分,通常是因為一個對象調(diào)用了一個不屬于它方法的方法導(dǎo)致的。
方法調(diào)用流程
runtime中具體的方法調(diào)用流程大致如下:
- 1、在相應(yīng)操作的對象中的緩存方法列表中找調(diào)用的方法,如果找到,轉(zhuǎn)向相應(yīng)實現(xiàn)并執(zhí)行。
- 2、如果沒找到,在相應(yīng)操作的對象中的方法列表中找調(diào)用的方法,如果找到,轉(zhuǎn)向相應(yīng)實現(xiàn)執(zhí)行
- 3、如果沒找到,去父類指針?biāo)赶虻膶ο笾袌?zhí)行1,2.
- 4、以此類推,如果一直到根類還沒找到,轉(zhuǎn)向攔截調(diào)用,走消息轉(zhuǎn)發(fā)機制。
- 5、如果沒有重寫攔截調(diào)用的方法,程序報錯。
在一個函數(shù)找不到時,runtime提供了三種方式去補救:
- 1、調(diào)用resolveInstanceMethod給個機會讓類添加這個實現(xiàn)這個函數(shù)
- 2、調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個函數(shù)
- 3、調(diào)用forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其他形式執(zhí)行。
通過重寫NSObject的forwardingTargetForSelector方法,我們就可以將無法識別的方法進行攔截并且將消息轉(zhuǎn)發(fā)到安全的樁類對象中,從而可以使app繼續(xù)正常運行
KVO crash 產(chǎn)生原因
KVO,即:Key-Value Observing,它提供一種機制,當(dāng)指定的對象的屬性被修改后,則對象就會接受收到通知。簡單的說就是每次指定的被觀察的對象的屬性被修改后,KVO就會自動通知相應(yīng)的觀察者了。
KVO機制在iOS的很多開發(fā)場景中都會被使用到。不過如果一不小心使用不當(dāng)?shù)脑?,會?dǎo)致大量的crash問題
通過會導(dǎo)致KVO Crash的兩種情形
- 1、KVO的被觀察者dealloc時仍然注冊著KVO導(dǎo)致的crash
- 2、添加KVO重復(fù)添加觀察者或重復(fù)移除觀察者(KVO注冊觀察者與移除觀察者不匹配)導(dǎo)致的crash
解決方法:可以讓被觀察對象持有一個KVO的delegate,所有和KVO相關(guān)的操作均通過delegate來進行管理,delegate通過建立一張map來維護KVO整個關(guān)系。具體就是使用runTime的交換方法重寫KVO的一些方法
NSNotification類型crash防護
當(dāng)一個對象添加了notification之后,如果dealloc的時候,仍然持有notification,就會出現(xiàn)NSNotification類型的crash。
NSNotification類型的crash多產(chǎn)生于程序員寫代碼時候犯疏忽,在NSNotificationCenter添加一個對象為observer之后,忘記了在對象dealloc的時候移除它。
所幸的是,蘋果在iOS9之后專門針對于這種情況做了處理,所以在iOS9之后,即使開發(fā)者沒有移除observer,Notification crash也不會再產(chǎn)生了。
不過針對于iOS9之前的用戶,我們還是有必要做一下NSNotification Crash的防護的。
NSNotification Crash的防護原理很簡單, 利用method swizzling hook NSObject的dealloc函數(shù),再對象真正dealloc之前先調(diào)用一下[[NSNotificationCenter defaultCenter] removeObserver:self]即可。
NSTimer類型crash防護
在程序開發(fā)過程中,大家會經(jīng)常使用定時任務(wù),但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:接口做重復(fù)性的定時任務(wù)時存在一個問題:NSTimer會強引用target實例,所以需要在合適的時機invalidate定時器,否則就會由于定時器timer強引用target的關(guān)系導(dǎo)致target不能被釋放,造成內(nèi)存泄露,甚至在定時任務(wù)觸發(fā)時導(dǎo)致crash。 crash的展現(xiàn)形式和具體的target執(zhí)行的selector有關(guān)。
與此同時,如果NSTimer是無限重復(fù)的執(zhí)行一個任務(wù)的話,也有可能導(dǎo)致target的selector一直被重復(fù)調(diào)用且處于無效狀態(tài),對app的CPU,內(nèi)存等性能方面均是沒有必要的浪費。
那么解決NSTimer的問題的關(guān)鍵點在于以下兩點:
- 1、NSTimer對其target是否可以不強引用
- 2、是否找到一個合適的時機,在確定NSTimer已經(jīng)失效的情況下,讓NSTimer自動invalidate
Container crash 防護方案
Container crash 類型的防護方案也比較簡單,針對于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的會導(dǎo)致崩潰的API進行method swizzling,然后在swizzle的新方法中加入一些條件限制和判斷,從而讓這些API變的安全
野指針crash 防護方案
野指針問題的解決思路方向其實很容易確定,XCode提供了Zombie的機制來排查野指針的問題,那么我們這邊可以實現(xiàn)一個類似于Zombie的機制,加上對zombie實例的全部方法攔截機制 和 消息轉(zhuǎn)發(fā)機制,那么就可以做到在野指針訪問時不Crash而只是crash時相關(guān)的信息。
同時還需要注意一點:因為zombie的機制需要在對象釋放時保留其指針和相關(guān)內(nèi)存占用,隨著app的進行,越來越多的對象被創(chuàng)建和釋放,這會導(dǎo)致內(nèi)存占用越來越大,這樣顯然對于一個正常運行的app的性能有影響。所以需要一個合適的zombie對象釋放機制,確定zombie機制對內(nèi)存的影響是有限度的
非主線程刷UI類型crash防護
在非主線程刷UI將會導(dǎo)致app運行crash,有必要對其進行處理。
目前初步的處理方案是swizzle UIView類的以下三個方法:
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
在這三個方法調(diào)用的時候判斷一下當(dāng)前的線程,如果不是主線程的話,直接利用 dispatch_async(dispatch_get_main_queue(), ^{ //調(diào)用原本方法 });
來將對應(yīng)的刷UI的操作轉(zhuǎn)移到主線程上,同時統(tǒng)計錯誤信息。
但是真正實施了之后,發(fā)現(xiàn)這三個方法并不能完全覆蓋UIView相關(guān)的所有刷UI到操作,但是如果要將全部到UIView的刷UI的方法統(tǒng)計起來并且swizzle,感覺略笨拙而且不高效。