1屎即、Crash產(chǎn)生原因

Crash我們不得不面對的問題庙睡,但是好多人在遇到Crash的時候都無從下手,很多的時候都是憑著感覺找問題技俐。今天我做了5篇文章來幫助我們更加清晰的認(rèn)清iOS中的Crash

想要了解更詳細(xì)的內(nèi)容可以點擊這里

Crash分類

一般是由 Mach異尘模或 Objective-C 異常(NSException)引起的谤逼。我們可以針對這兩種情況抓取對應(yīng)的 Crash 事件

crash2.png
  • 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異常方式

crash.png

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要提防二次采集的可能晃择。

crash3.png

處理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);
}

代碼參考至向晨宇的技術(shù)博客-iOS異常捕獲

demo地址

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,感覺略笨拙而且不高效贾费。

漫談 iOS Crash 收集框架

全面的理解和分析iOS的崩潰日志

iOS實錄14:淺談iOS Crash(一)

質(zhì)量監(jiān)控-保護(hù)你的crash

深入iOS系統(tǒng)底層之crash解決方法介紹

Baymax:網(wǎng)易iOS App運(yùn)行時Crash自動防護(hù)實踐

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钦购,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子褂萧,更是在濱河造成了極大的恐慌押桃,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件导犹,死亡現(xiàn)場離奇詭異唱凯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谎痢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進(jìn)店門磕昼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人节猿,你說我怎么就攤上這事票从。” “怎么了滨嘱?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵峰鄙,是天一觀的道長。 經(jīng)常有香客問我九孩,道長先馆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任躺彬,我火速辦了婚禮煤墙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宪拥。我一直安慰自己仿野,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布她君。 她就那樣靜靜地躺著脚作,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缔刹。 梳的紋絲不亂的頭發(fā)上球涛,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機(jī)與錄音校镐,去河邊找鬼亿扁。 笑死,一個胖子當(dāng)著我的面吹牛鸟廓,可吹牛的內(nèi)容都是我干的从祝。 我是一名探鬼主播襟己,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼牍陌!你這毒婦竟也來了擎浴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤毒涧,失蹤者是張志新(化名)和其女友劉穎贮预,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體链嘀,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡萌狂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了怀泊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茫藏。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖霹琼,靈堂內(nèi)的尸體忽然破棺而出务傲,到底是詐尸還是另有隱情,我是刑警寧澤枣申,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布售葡,位于F島的核電站,受9級特大地震影響忠藤,放射性物質(zhì)發(fā)生泄漏挟伙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一模孩、第九天 我趴在偏房一處隱蔽的房頂上張望尖阔。 院中可真熱鬧,春花似錦榨咐、人聲如沸介却。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽齿坷。三九已至,卻和暖如春数焊,著一層夾襖步出監(jiān)牢的瞬間永淌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工佩耳, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留仰禀,地道東北人。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓蚕愤,卻偏偏與公主長得像答恶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子萍诱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容

  • [這是第14篇] 序: iOS Crash問題是iOS開發(fā)中難以忽視的存在悬嗓,本文就捕獲iOS Crash、Cras...
    南華coder閱讀 9,883評論 21 116
  • 一裕坊、前言 在日常開發(fā)中或者測試過程中包竹,我們的應(yīng)用可能會出現(xiàn)Crash的問題。對于這類問題我們要抱著零容忍的態(tài)度籍凝,因...
    WQ_UESTC閱讀 5,751評論 4 39
  • 轉(zhuǎn)載(漫談 iOS Crash 收集框架) 前言 很早以前就和念茜認(rèn)識周瞎,念茜不但技術(shù)功底扎實,而且長得很漂亮饵蒂,說她...
    狂風(fēng)無跡閱讀 3,309評論 1 11
  • 本文就捕獲iOS Crash声诸、Crash日志組成、Crash日志符號化退盯、異常信息解讀彼乌、常見的Crash五部分介紹。...
    xukuangbo_閱讀 1,582評論 0 0
  • 盡管我知道渊迁,在這個世上慰照,我不能相信任何人,但是我就是義無反顧的愛上你琉朽,你是我生命中的希望毒租,也是我最大的錯誤,這個世...
    浮夢依云閱讀 128評論 0 1