前言
在iOS開(kāi)發(fā)過(guò)程中,總會(huì)遇到各種各樣的崩潰問(wèn)題,那么如何可能的降低應(yīng)用的崩潰率,就成為每位iOS開(kāi)發(fā)人員的必修課。所以,歸納總結(jié)iOS崩潰問(wèn)題就顯得尤為重要了。
crash類型
1 OC層面的crash
- 找不到方法的實(shí)現(xiàn)unrecognized selector
- KVC崩潰
- KVO崩潰
- NSInvalidArgumentException: 非法參數(shù)異常,傳入非法參數(shù)導(dǎo)致異常,nil參數(shù)比較常見(jiàn)
- NSRangeException: 下標(biāo)越界導(dǎo)致的異常
- NSGenericException: foreach的循環(huán)當(dāng)中修改元素導(dǎo)致的異常
2 Signal層面的crash
除了OC層面的異常捕獲之外,還有很多崩潰則需要利用unix標(biāo)準(zhǔn)的signal機(jī)制,注冊(cè)SIGABRT, SIGBUS, SIGSEGV等信號(hào)發(fā)生時(shí)的處理函數(shù)。該函數(shù)中我們可以輸出棧信息,版本信息等其他一切我們所想要的。這個(gè)unix標(biāo)準(zhǔn)的signal又是如何產(chǎn)生的呢?主要來(lái)源于這兩類:
- 軟件異常:軟件異常主要來(lái)自 kill(),pthread_kill()。iOS 中的 NSException 未捕獲,abort都屬于這種情況。
- 硬件異常:硬件的信號(hào)始于處理器 trap,是和平臺(tái)相關(guān)的。野指針崩潰大部分是硬件異常。
其中,硬件異常的流程是:硬件異常 -> Mach異常 -> Unix信號(hào)。根據(jù)《Mac OS X Internals》中說(shuō)到的bsd相關(guān)的處理代碼 ux_exception.c ,可以看到 Mach 異常和 Unix 信號(hào)存在的對(duì)應(yīng)關(guān)系。

2.1 Mach 異常
- EXC_BAD_ACCESS: 不能訪問(wèn)的內(nèi)存
- EXC_BAD_INSTRUCTION: 非法或未定義的指令或操作數(shù)
- EXC_ARITHMETIC: 算術(shù)異常(例如除以0)。iOS 默認(rèn)是不啟用的,所以我們一般不會(huì)遇到
- EXC_EMULATION: 執(zhí)行打算用于支持仿真的指令
- EXC_SOFTWARE:軟件生成的異常,我們?cè)?Crash 日志中一般不會(huì)看到這個(gè)類型,蘋(píng)果的日志里會(huì)是 EXC_CRASH
- EXC_BREAKPOINT:跟蹤或斷點(diǎn)
- EXC_SYSCALL: UNIX 系統(tǒng)調(diào)用
- EXC_MACH_SYSCALL: Mach 系統(tǒng)調(diào)用
2.2 UNIX 信號(hào):
- SIGSEGV,段錯(cuò)誤。訪問(wèn)未分配內(nèi)存、寫(xiě)入沒(méi)有寫(xiě)權(quán)限的內(nèi)存等。
- SIGBUS,總線錯(cuò)誤。比如內(nèi)存地址對(duì)齊、錯(cuò)誤的內(nèi)存類型訪問(wèn)等。
- SIGILL,執(zhí)行了非法指令,一般是可執(zhí)行文件出現(xiàn)了錯(cuò)誤。
- SIGFPE ,致命的算術(shù)運(yùn)算。比如數(shù)值溢出、NaN數(shù)值等。
- SIGABRT,調(diào)用 abort() 產(chǎn)生,通過(guò) pthread_kill() 發(fā)送。
- SIGPIPE,管道破裂。通常在進(jìn)程間通信產(chǎn)生。比如采用FIFO(管道)通信的兩個(gè)進(jìn)程,讀管道沒(méi)打開(kāi)或者意外終止就往管道寫(xiě),寫(xiě)進(jìn)程會(huì)收到SIGPIPE信號(hào)。根據(jù)蘋(píng)果相關(guān)文檔,可以忽略這個(gè)信號(hào)。
- SIGSYS,系統(tǒng)調(diào)用異常。
- SIGKILL,此信號(hào)表示系統(tǒng)中止進(jìn)程。崩潰報(bào)告會(huì)包含代表中止原因的編碼。exit(), kill(9) 等函數(shù)調(diào)用。iOS 系統(tǒng)殺進(jìn)程,如 watchDog 殺進(jìn)程。
- SIGTRAP,斷點(diǎn)指令或者其他trap指令產(chǎn)生。
崩潰原因與解決方案
對(duì)于OC層面的崩潰問(wèn)題,都是代碼不夠嚴(yán)謹(jǐn)導(dǎo)致,收集到崩潰后找到具體的出錯(cuò)位置,問(wèn)題自然也就迎刃而解了。但是,對(duì)于Signal層面的崩潰,主要是由于野指針和OOM引起,讓開(kāi)發(fā)人員頭疼不已的,這里就對(duì)Signal部分的崩潰問(wèn)題進(jìn)一步探討探討。
1 野指針
野指針,指向一個(gè)已刪除的對(duì)象或未申請(qǐng)?jiān)L問(wèn)受限內(nèi)存區(qū)域的指針。而iOS中的野指針,一般都是對(duì)象釋放之后指針未置空導(dǎo)致的野指針(iOS里面一般不會(huì)出現(xiàn)為初始化對(duì)象的常識(shí)性錯(cuò)誤)。
騰訊Bugly團(tuán)隊(duì)對(duì)野指針情況做一下分類:

部分野指針問(wèn)題并不是按照某個(gè)操作步驟就能夠復(fù)現(xiàn),那如何才能定位野指針問(wèn)題的具體位置呢?可以通過(guò)Xcode提供的Malloc Scribble 和 Zombie Objects。
1.1 Malloc Scribble
設(shè)置開(kāi)啟Malloc Scribble,申請(qǐng)內(nèi)存 alloc 時(shí)在內(nèi)存上填0xAA,釋放內(nèi)存 dealloc 在內(nèi)存上填 0x55。如果內(nèi)存未被初始化就被訪問(wèn)或者釋放后被訪問(wèn),那么Crash必現(xiàn)。
1.2 代碼實(shí)現(xiàn)Malloc Scribble
開(kāi)發(fā)人員可以通過(guò)Xcode使用Malloc Scribble,那么測(cè)試同學(xué)如果想用Malloc Scribble進(jìn)行測(cè)試驗(yàn)證怎么辦呢?總不能要求測(cè)試同學(xué)安裝Xcode編譯代碼吧。所以,就得想想如何通過(guò)代碼實(shí)現(xiàn)Malloc Scribble功能,即想辦法通過(guò)代碼實(shí)現(xiàn)指針指向的對(duì)象在釋放時(shí)在其內(nèi)存上填入0x55。
1.2.1 OC對(duì)象的釋放流程
通過(guò)runtime源碼可以查看到NSObject對(duì)象的dealloc處理流程是:
- 首先調(diào)用_objc_rootDealloc()
- 然后調(diào)用rootDealloc()
- 判斷是否可以被釋放,判斷依據(jù)為,是否有以下5中情況:
(1)NONPointer_ISA
(2)weakly_reference
(3)has_assoc
(4)has_cxx_dtor
(5)has_sidetable_rc - 如果有以上5中情況中的任意一種,則調(diào)用object_dispose()方法;如果沒(méi)有其中任意一種,表明可以執(zhí)行釋放操作,執(zhí)行C函數(shù)的free()。
1.2.2 實(shí)現(xiàn)Malloc Scribble功能
通過(guò)上面的dealloc處理流程可以看出,NSObject對(duì)象的釋放最終需要調(diào)用C函數(shù)的free()方法,而且C對(duì)象的釋放也需要調(diào)用free()方法。所以,可以利用 fishHook 直接 hook 掉 free() 方法就可以實(shí)現(xiàn)Malloc Scribble功能。
void safe_free(void* p) {
size_tmemSiziee=malloc_size(p);
memset(p,0x55, memSiziee);
orig_free(p);
return;
}
當(dāng)然,如果代碼中不涉及C語(yǔ)音,也可以對(duì)dealloc()、rootDealloc() 或者 object_dispose() 進(jìn)行hook操作。
1.3 Zombie Objects
僵尸對(duì)象,可用于檢測(cè)內(nèi)存錯(cuò)誤(EXC_BAD_ACCESS),給僵尸對(duì)象發(fā)送消息的話,它仍然可以響應(yīng),然后將會(huì)發(fā)生崩潰,并輸出錯(cuò)誤日志來(lái)顯示野指針對(duì)象調(diào)用的類名和方法。
1.4 Malloc scribble與Zombie Objects對(duì)比
Zombie Objects相對(duì)Malloc scribble的優(yōu)勢(shì),就是不用考慮不會(huì)崩潰的情況,只要野指針指向僵尸對(duì)象,再次訪問(wèn)就一定會(huì)崩潰。
Zombie Objects相對(duì)Malloc scribble的劣勢(shì),就是不如 Malloc scribble 方案中覆蓋的范圍廣,可以通過(guò) hook free() 的方式將 C 語(yǔ)音編碼部分也包含在內(nèi)。
2 OOM問(wèn)題
OOM,Out Of Memory,是指在 iOS 設(shè)備上應(yīng)用因?yàn)閮?nèi)存占用過(guò)高而被操作系統(tǒng)的Jetsam機(jī)制強(qiáng)制終止,即用戶所感知到的應(yīng)用閃退。這類崩潰的日志一般都是Jetsam開(kāi)頭,無(wú)法用于定位問(wèn)題的根源所在。
2.1 Memory Warning
每個(gè)UIViewController都有一個(gè)didReceivedMemoryWarning的方法。當(dāng)使用的內(nèi)存是一點(diǎn)點(diǎn)上漲時(shí),而不是一下子直接把內(nèi)存撐爆。在達(dá)到內(nèi)存臨界點(diǎn)之前,系統(tǒng)會(huì)給各個(gè)正在運(yùn)行的應(yīng)用發(fā)出內(nèi)存警告,告知app去清理自己的內(nèi)存。而內(nèi)存警告,并不總是由于自身app導(dǎo)致的。
出現(xiàn)OOM前一定會(huì)出現(xiàn)Memory Warning么? 答案是不一定,有可能瞬間申請(qǐng)了大量?jī)?nèi)存,而恰好此時(shí)主線程在忙于其他事情,導(dǎo)致可能沒(méi)有經(jīng)歷過(guò)Memory Warning就發(fā)生了OOM。即便觸發(fā)了多次Memory Warning,也不一定會(huì)出現(xiàn)OOM。
2.2 如何確定OOM的閾值
必須要明確的一點(diǎn)是,不同iOS設(shè)備的OOM閾值是不同的。
2.2.1 通過(guò)Jetsam日志獲取閥值
從應(yīng)用被Jetsam機(jī)制終止時(shí)生成的日志中,計(jì)算rpages * pageSize即可得到OOM的閾值,單位上byte。
2.2.2 通過(guò)Memory Warning日志獲取閥值
Memory Warning警告的EXC_RESOURCE_EXCEPTION異常也包含了OOM閾值信息。
2.2.3 phys_footprint
通過(guò)task_vm_info_data_t的phys_footprint可以獲取到一個(gè)OOM閥值,然后通過(guò)代碼循環(huán)申請(qǐng)內(nèi)存,查看應(yīng)用在OOM發(fā)生時(shí)的內(nèi)存消耗情況OOM閥值。
2.2.4 os_proc_available_memory
iOS13系統(tǒng)os/proc.h中os_proc_available_memory方法,可以查看當(dāng)前可用內(nèi)存。
2.3 如何判定OOM的發(fā)生
facebook和微信的Matrix都是采用的排除法。在Matrix初始化的時(shí)候調(diào)用checkRebootType方法,來(lái)判定是否發(fā)生了OOM,具體流程如下:
- 如果當(dāng)前設(shè)備正在DEBUG,則直接返回,不繼續(xù)執(zhí)行。
- 上次打開(kāi)app是否發(fā)生了普通的崩潰,如果不是繼續(xù)執(zhí)行。
- 上次打開(kāi)app后,是用戶是否主動(dòng)退出的應(yīng)用(監(jiān)聽(tīng)UIApplicationWillTerminateNotification消息),如果不是繼續(xù)執(zhí)行。
- 上次打開(kāi)app后,是否調(diào)用exit相關(guān)的函數(shù)(通過(guò)atexit函數(shù)監(jiān)控),如果不是繼續(xù)執(zhí)行
- 上次打開(kāi)app后,app是否掛起suspend或者執(zhí)行backgroundFetch,如果此時(shí)沒(méi)有被看門狗殺死,則是一種OOM,Matrix起名叫Suspend OOM,如果不是繼續(xù)執(zhí)行
- app的uuid是否變化了,如果不是繼續(xù)執(zhí)行
- 上次打開(kāi)app后,系統(tǒng)是否升級(jí)了,如果不是繼續(xù)執(zhí)行
- 上次打開(kāi)app后,設(shè)備是否重啟了,如果不是繼續(xù)執(zhí)行
- 上次打開(kāi)app時(shí),app是否處于后臺(tái),如果是,則觸發(fā)了Background OOM,如果不是繼續(xù)執(zhí)行
- 上次打開(kāi)app后,app是否處于前臺(tái),是否主線程卡死了,如果沒(méi)有卡死,則說(shuō)明觸發(fā)了Foreground OOM。