iOS崩潰問(wèn)題總結(jié)

前言

在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)系。

Mach 異常.png

2.1 Mach 異常

  1. EXC_BAD_ACCESS: 不能訪問(wèn)的內(nèi)存
  2. EXC_BAD_INSTRUCTION: 非法或未定義的指令或操作數(shù)
  3. EXC_ARITHMETIC: 算術(shù)異常(例如除以0)。iOS 默認(rèn)是不啟用的,所以我們一般不會(huì)遇到
  4. EXC_EMULATION: 執(zhí)行打算用于支持仿真的指令
  5. EXC_SOFTWARE:軟件生成的異常,我們?cè)?Crash 日志中一般不會(huì)看到這個(gè)類型,蘋(píng)果的日志里會(huì)是 EXC_CRASH
  6. EXC_BREAKPOINT:跟蹤或斷點(diǎn)
  7. EXC_SYSCALL: UNIX 系統(tǒng)調(diào)用
  8. EXC_MACH_SYSCALL: Mach 系統(tǒng)調(diào)用

2.2 UNIX 信號(hào):

  1. SIGSEGV,段錯(cuò)誤。訪問(wèn)未分配內(nèi)存、寫(xiě)入沒(méi)有寫(xiě)權(quán)限的內(nèi)存等。
  2. SIGBUS,總線錯(cuò)誤。比如內(nèi)存地址對(duì)齊、錯(cuò)誤的內(nèi)存類型訪問(wèn)等。
  3. SIGILL,執(zhí)行了非法指令,一般是可執(zhí)行文件出現(xiàn)了錯(cuò)誤。
  4. SIGFPE ,致命的算術(shù)運(yùn)算。比如數(shù)值溢出、NaN數(shù)值等。
  5. SIGABRT,調(diào)用 abort() 產(chǎn)生,通過(guò) pthread_kill() 發(fā)送。
  6. 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)。
  7. SIGSYS,系統(tǒng)調(diào)用異常。
  8. SIGKILL,此信號(hào)表示系統(tǒng)中止進(jìn)程。崩潰報(bào)告會(huì)包含代表中止原因的編碼。exit(), kill(9) 等函數(shù)調(diào)用。iOS 系統(tǒng)殺進(jìn)程,如 watchDog 殺進(jìn)程。
  9. 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ì)野指針情況做一下分類:

Pasted Graphic 2.png

部分野指針問(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處理流程是:

  1. 首先調(diào)用_objc_rootDealloc()
  2. 然后調(diào)用rootDealloc()
  3. 判斷是否可以被釋放,判斷依據(jù)為,是否有以下5中情況:
    (1)NONPointer_ISA
    (2)weakly_reference
    (3)has_assoc
    (4)has_cxx_dtor
    (5)has_sidetable_rc
  4. 如果有以上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,具體流程如下:

  1. 如果當(dāng)前設(shè)備正在DEBUG,則直接返回,不繼續(xù)執(zhí)行。
  2. 上次打開(kāi)app是否發(fā)生了普通的崩潰,如果不是繼續(xù)執(zhí)行。
  3. 上次打開(kāi)app后,是用戶是否主動(dòng)退出的應(yīng)用(監(jiān)聽(tīng)UIApplicationWillTerminateNotification消息),如果不是繼續(xù)執(zhí)行。
  4. 上次打開(kāi)app后,是否調(diào)用exit相關(guān)的函數(shù)(通過(guò)atexit函數(shù)監(jiān)控),如果不是繼續(xù)執(zhí)行
  5. 上次打開(kāi)app后,app是否掛起suspend或者執(zhí)行backgroundFetch,如果此時(shí)沒(méi)有被看門狗殺死,則是一種OOM,Matrix起名叫Suspend OOM,如果不是繼續(xù)執(zhí)行
  6. app的uuid是否變化了,如果不是繼續(xù)執(zhí)行
  7. 上次打開(kāi)app后,系統(tǒng)是否升級(jí)了,如果不是繼續(xù)執(zhí)行
  8. 上次打開(kāi)app后,設(shè)備是否重啟了,如果不是繼續(xù)執(zhí)行
  9. 上次打開(kāi)app時(shí),app是否處于后臺(tái),如果是,則觸發(fā)了Background OOM,如果不是繼續(xù)執(zhí)行
  10. 上次打開(kāi)app后,app是否處于前臺(tái),是否主線程卡死了,如果沒(méi)有卡死,則說(shuō)明觸發(fā)了Foreground OOM。

參考文章

iOS 野指針處理
如何定位Obj-C野指針隨機(jī)Crash
深入了解iOS中的OOM

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、獲取 Crash、dSYM 文件 獲取到的 .ips 改后綴為 .crash 即可 真機(jī) Crash 文件目錄...
    midmirror閱讀 9,954評(píng)論 0 31
  • 前言 iOS崩潰是讓iOS開(kāi)發(fā)人員比較頭痛的事情,app崩潰了,說(shuō)明代碼寫(xiě)的有問(wèn)題,這時(shí)如何快速定位到崩潰的地方很...
    齊滇大圣閱讀 65,887評(píng)論 29 443
  • 崩潰分析 崩潰日志(crash log) 根據(jù)符號(hào)表來(lái)監(jiān)測(cè)崩潰位置 什么是符號(hào)表符號(hào)表就是指在Xcode項(xiàng)目編譯后...
    紙簡(jiǎn)書(shū)生閱讀 5,919評(píng)論 0 17
  • [這是第15篇] 導(dǎo)語(yǔ):在當(dāng)前的iOS開(kāi)發(fā)中,雖然ARC為開(kāi)發(fā)者解決了手動(dòng)內(nèi)存管理時(shí)代 的許多麻煩,但是內(nèi)存方面的...
    南華coder閱讀 7,749評(píng)論 10 78
  • 前言 iOS崩潰是讓iOS開(kāi)發(fā)人員比較頭痛的事情,app崩潰了,說(shuō)明代碼寫(xiě)的有問(wèn)題,這時(shí)如何快速定位到崩潰的地方很...
    lp_lp閱讀 5,081評(píng)論 0 16

友情鏈接更多精彩內(nèi)容