Crash我們不得不面對的問題庙睡,但是好多人在遇到Crash的時候都無從下手,很多的時候都是憑著感覺找問題技俐。今天我做了5篇文章來幫助我們更加清晰的認(rèn)清iOS中的Crash
想要了解更詳細(xì)的內(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)中的一種異步通知機(jī)制戚绕,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異常可以在OC層得到處理端盆;如果OC異常一直得不到處理怀骤,程序會強(qiáng)行發(fā)送SIGABRT信號中斷程序费封。在OC層如果沒有對應(yīng)的NSException焕妙,就只能讓Unix標(biāo)準(zhǔn)的signal機(jī)制來處理了。
- 5弓摘、在捕獲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異常
Mach操作系統(tǒng)微內(nèi)核璧针,是許多新操作系統(tǒng)的設(shè)計基礎(chǔ)。Mach微內(nèi)核中有幾個基礎(chǔ)概念:
- Tasks渊啰,擁有一組系統(tǒng)資源的對象探橱,允許"thread"在其中執(zhí)行。
- Threads绘证,執(zhí)行的基本單位隧膏,擁有task的上下文,并共享其資源嚷那。
- Ports胞枕,task之間通訊的一組受保護(hù)的消息隊列;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異常可以在OC層得到處理浪蹂;如果OC異常一直得不到處理抵栈,程序會強(qiáng)行發(fā)送SIGABRT信號中斷程序。在OC層如果沒有對應(yīng)的NSException乌逐,就只能讓Unix標(biāo)準(zhǔn)的signal機(jī)制來處理了
在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ù)運(yùn)算倦零。比如數(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(機(jī)制待改善))
更加詳細(xì)的信息請參考Baymax:網(wǎng)易iOS App運(yùn)行時Crash自動防護(hù)實踐
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ā)機(jī)制。
- 5宙枷、如果沒有重寫攔截調(diào)用的方法掉房,程序報錯茧跋。
在一個函數(shù)找不到時,runtime提供了三種方式去補(bǔ)救:
- 1卓囚、調(diào)用resolveInstanceMethod給個機(jī)會讓類添加這個實現(xiàn)這個函數(shù)
- 2瘾杭、調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個函數(shù)
- 3、調(diào)用forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標(biāo)函數(shù)以其他形式執(zhí)行哪亿。
通過重寫NSObject的forwardingTargetForSelector方法粥烁,我們就可以將無法識別的方法進(jìn)行攔截并且將消息轉(zhuǎn)發(fā)到安全的樁類對象中,從而可以使app繼續(xù)正常運(yùn)行
KVO crash 產(chǎn)生原因
KVO,即:Key-Value Observing蝇棉,它提供一種機(jī)制页徐,當(dāng)指定的對象的屬性被修改后,則對象就會接受收到通知银萍。簡單的說就是每次指定的被觀察的對象的屬性被修改后变勇,KVO就會自動通知相應(yīng)的觀察者了。
KVO機(jī)制在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來進(jìn)行管理,delegate通過建立一張map來維護(hù)KVO整個關(guān)系瓶您。具體就是使用runTime的交換方法重寫KVO的一些方法
NSNotification類型crash防護(hù)
當(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的防護(hù)的。
NSNotification Crash的防護(hù)原理很簡單细办, 利用method swizzling hook NSObject的dealloc函數(shù)橙凳,再對象真正dealloc之前先調(diào)用一下[[NSNotificationCenter defaultCenter] removeObserver:self]即可。
NSTimer類型crash防護(hù)
在程序開發(fā)過程中,大家會經(jīng)常使用定時任務(wù)痕惋,但使用NSTimer的 scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:接口做重復(fù)性的定時任務(wù)時存在一個問題:NSTimer會強(qiáng)引用target實例区宇,所以需要在合適的時機(jī)invalidate定時器,否則就會由于定時器timer強(qiáng)引用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是否可以不強(qiáng)引用
- 2胜宇、是否找到一個合適的時機(jī),在確定NSTimer已經(jīng)失效的情況下恢着,讓NSTimer自動invalidate
Container crash 防護(hù)方案
Container crash 類型的防護(hù)方案也比較簡單桐愉,針對于NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache的一些常用的會導(dǎo)致崩潰的API進(jìn)行method swizzling,然后在swizzle的新方法中加入一些條件限制和判斷掰派,從而讓這些API變的安全
野指針crash 防護(hù)方案
野指針問題的解決思路方向其實很容易確定从诲,XCode提供了Zombie的機(jī)制來排查野指針的問題,那么我們這邊可以實現(xiàn)一個類似于Zombie的機(jī)制靡羡,加上對zombie實例的全部方法攔截機(jī)制 和 消息轉(zhuǎn)發(fā)機(jī)制系洛,那么就可以做到在野指針訪問時不Crash而只是crash時相關(guān)的信息。
同時還需要注意一點:因為zombie的機(jī)制需要在對象釋放時保留其指針和相關(guān)內(nèi)存占用略步,隨著app的進(jìn)行描扯,越來越多的對象被創(chuàng)建和釋放,這會導(dǎo)致內(nèi)存占用越來越大纳像,這樣顯然對于一個正常運(yùn)行的app的性能有影響荆烈。所以需要一個合適的zombie對象釋放機(jī)制,確定zombie機(jī)制對內(nèi)存的影響是有限度的
非主線程刷UI類型crash防護(hù)
在非主線程刷UI將會導(dǎo)致app運(yùn)行crash竟趾,有必要對其進(jìn)行處理。
目前初步的處理方案是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,感覺略笨拙而且不高效贾费。