應(yīng)用程序崩潰是程序開發(fā)過程中除了bug外一直伴隨我們的最大的幽靈,時(shí)不時(shí)給我們致命一擊。
應(yīng)用程序崩潰的原因有很多,一般應(yīng)用程序在崩潰時(shí)會(huì)觸發(fā)相應(yīng)的異常退出信號(hào)氢卡。iOS應(yīng)用程序崩潰信號(hào)可以分為操作系統(tǒng)異常信號(hào),iOS系統(tǒng)限制以及語言運(yùn)行時(shí)錯(cuò)誤晨缴。
操作系統(tǒng)常見異常
由于iOS源自MacOS译秦,而MacOS由基于Unix并被Apple添加了很多自定義組件。在iOS系統(tǒng)中會(huì)出現(xiàn)兩種異常
Mach異常與UNIX異常
- Mach異常
Mach是一個(gè)XNU的微內(nèi)核核心击碗,Mach異常是指最底層的內(nèi)核級(jí)異常筑悴。每個(gè)thread,task稍途,host都有一個(gè)異常端口數(shù)組阁吝,Mach的部分API暴露給了用戶態(tài),用戶態(tài)的開發(fā)者可以直接通過Mach API設(shè)置thread械拍,task突勇,host的異常端口,來捕獲Mach異常坷虑,抓取Crash事件甲馋。
所有Mach異常都在host層被ux_exception轉(zhuǎn)換為相應(yīng)的Unix信號(hào),并通過threadsignal將信號(hào)投遞到出錯(cuò)的線程猖吴。iOS中的 POSIX API就是通過Mach之上的 BSD層實(shí)現(xiàn)的。
Mach微內(nèi)核中有幾個(gè)基礎(chǔ)概念:
Tasks挥转,擁有一組系統(tǒng)資源的對象海蔽,允許"thread"在其中執(zhí)行。
Threads绑谣,執(zhí)行的基本單位党窜,擁有task的上下文,并共享其資源借宵。
Ports幌衣,task之間通訊的一組受保護(hù)的消息隊(duì)列;task可對任何port發(fā)送/接收數(shù)據(jù)。
Host豁护, Mach 最基礎(chǔ)的對象是“主機(jī)(host)”哼凯,也就是表示機(jī)器本身的對象。主機(jī)對象是一個(gè)簡單的數(shù)據(jù)結(jié)構(gòu)楚里。主機(jī)只不過是一組“特殊端口”的集合(用于向主機(jī)發(fā)送各種消息)断部,以及一組異常處理程序的集合。主機(jī)定義了一個(gè)鎖組用于保護(hù)異常處理的并發(fā)訪問班缎。主機(jī)的數(shù)據(jù)結(jié)構(gòu)主要有三個(gè)基本功能:
提供機(jī)器信息:Mach 提供了一組異常豐富的API調(diào)用用于查詢機(jī)器信息蝴光,所有這些調(diào)用都要求獲得主機(jī)端口才能工作。
提供子系統(tǒng)的訪問:通過主機(jī)抽象达址,應(yīng)用程序可以請求訪問子系統(tǒng)使用的任何“特殊”端口蔑祟。此外,還可以獲得所有其他機(jī)器抽象(例如:processor 和 processor_set)的訪問權(quán)沉唠。
提供默認(rèn)的異常處理:異常從線程基本提升到進(jìn)程(任務(wù))基本疆虚,如果沒有被處理的話。則進(jìn)一步提升到主機(jī)級(jí)別做通用的處理右冻。
- UNIX信號(hào)
這就是我們常說的信號(hào)了装蓬,實(shí)際上由于Mach異常會(huì)被轉(zhuǎn)換為Unix信號(hào),通常我們看到的crash都是觸發(fā)為unix異常信號(hào)纱扭。常見的Unix信號(hào)如下:
信號(hào) | 說明 |
---|---|
SIGHUP | 用戶終端退出進(jìn)程時(shí)牍帚,終端將接收到SIGHUP信號(hào)。這個(gè)信號(hào)的默認(rèn)操作為終止進(jìn)程乳蛾。iOS中通常不出現(xiàn)該信號(hào) |
SIGTERM | 終止請求暗赶,發(fā)送到程序(軟kill),iOS中通常不出現(xiàn)該信號(hào) |
SIGSEGV | 無效的內(nèi)存訪問(分段故障) |
SIGINT | 外部中斷肃叶,通常由用戶發(fā)起蹂随,程序終止信號(hào)(interrupt),在用戶鍵入INTR字符(通常Ctrl-C)時(shí)發(fā)出因惭,用于通知前臺(tái)進(jìn)程組終止進(jìn)程岳锁。iOS中通常不出現(xiàn)該信號(hào) |
SIGILL | 無效的程序映像,如無效指令蹦魔,通常是kill命令調(diào)用產(chǎn)生(強(qiáng)制kill)激率,iOS下殺死app會(huì)走應(yīng)用程序周期,一般不出現(xiàn)該信號(hào) |
SIGABRT | 異常終止條件勿决,例如由中止abort函數(shù)調(diào)用 |
SIGFPE | 錯(cuò)誤的算術(shù)運(yùn)算乒躺,如除以零 |
SIGBUS | 試圖訪問一塊無文件內(nèi)容對應(yīng)的內(nèi)存區(qū)域,比如超過文件尾的內(nèi)存區(qū)域低缩,或者以前有文件內(nèi)容對應(yīng)嘉冒,現(xiàn)在為另一進(jìn)程截?cái)噙^的內(nèi)存區(qū)域。 |
SIGTRAP | 常來說SIGTRAP是由斷點(diǎn)指令或其它trap指令產(chǎn)生. 由debugger使用。如果沒有附加調(diào)試器讳推,則該過程將終止并生成崩潰報(bào)告顶籽。 較低級(jí)的庫(例如,libdispatch)會(huì)在遇到致命錯(cuò)誤時(shí)捕獲進(jìn)程娜遵。 |
SIGABRT
在C/C++的庫中有較多觸發(fā)SIGABRT的場景
- 多次free指針
#include "stdlib.h"
#include "string.h"
#include "stdio.h
int main()
{
void *pc = malloc(1024);
free(pc);
//free(pc); //打開注釋會(huì)導(dǎo)致錯(cuò)誤
printf("free ok!\n");
return 0;
}
- C/C++中使用abort函數(shù)
#include "stdlib.h"
int main()
{
printf("before run abort!\n");
abort();
printf("after run abort!\n");
return 0;
}
assert函數(shù)內(nèi)部也是會(huì)調(diào)用abort蜕衡。在使用一些系統(tǒng)庫容易出現(xiàn)SIGABRT異常,在一些被禁止調(diào)用的函數(shù)被調(diào)用時(shí)也會(huì)出現(xiàn)該異常錯(cuò)誤设拟。
- 在C/C++中對容器類的越界訪問也會(huì)產(chǎn)生該信號(hào)Crash
(備注:OC中容器訪問越界會(huì)觸發(fā)SIGSEGV信號(hào)慨仿,或NSRangeException異常)
int i = 0;
int arr[3] = {0}; // 包含三個(gè) int 元素的數(shù)組,并且纳胧,每個(gè)元素的值初始化為 0
for (; i < 4; i++) { // i < 4, 這個(gè)地方會(huì)造成數(shù)組越界
arr[i] = i;
cout << "arr[" << i << "] = " << arr[i] << endl;
}
SIGSEGV
SIGSEGV錯(cuò)誤是我們應(yīng)用最常見的錯(cuò)誤镰吆,SIGSEGV在很多時(shí)候是由于指針越界引起的,但并不是所有的指針越界都會(huì)引發(fā)SIGSEGV跑慕。一個(gè)越界的指針万皿,如果不解引用它,是不會(huì)引起SIGSEGV的核行。而即使解引用了一個(gè)越界的指針牢硅,也不一定會(huì)引起SIGSEGV。
非法的內(nèi)存訪問
錯(cuò)誤的訪問類型
#include <stdio.h>
#include <stdlib.h>
int main() {
char* s = "hello world";
s[1] = 'H';
}
這是最常見的一個(gè)例子芝雪。此例中减余,”hello world”作為一個(gè)常量字符串,在編譯后會(huì)被放在.rodata節(jié)(GCC)惩系,最后鏈接生成目標(biāo)程序時(shí).rodata節(jié)會(huì)被合并到text segment與代碼段放在一起位岔,故其所處內(nèi)存區(qū)域是只讀的。
這就是錯(cuò)誤的訪問類型引起的SIGSEGV堡牡。
- 訪問非進(jìn)程空間內(nèi)存
內(nèi)存地址不在進(jìn)程的地址空間之內(nèi)
以空指針為代表的程序起始空間
未申請的堆空間
段與段之間的空洞
內(nèi)存地址空間合法抒抬,但是權(quán)限不滿足
對代碼段進(jìn)行寫操作:野指針,向代碼段進(jìn)行寫操作
對數(shù)據(jù)段進(jìn)行執(zhí)行操作:rip錯(cuò)誤晤柄,把數(shù)據(jù)段的數(shù)據(jù)當(dāng)作指令來執(zhí)行
訪問不存在內(nèi)存:invalid memory access (segmentation fault)擦剑,例如
char *p = NULL;
*p = 1;
多線程訪問同一內(nèi)存地址
函數(shù)跳轉(zhuǎn)到了一個(gè)非法的地址上執(zhí)行
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void foo () {
char c;
memset (&c, 0x55, 128);
}
int main () {
foo();
}
通過棧溢出,我們將函數(shù)foo的返回地址覆蓋成了0x55555555芥颈,函數(shù)跳轉(zhuǎn)到了一個(gè)非法地址執(zhí)行惠勒,最終引發(fā)SIGSEGV。
SIGBUS
一般是由于地址未對齊導(dǎo)致的浇借,例如內(nèi)存地址對齊出錯(cuò)捉撮,或者試圖執(zhí)行沒有權(quán)限的代碼地址怕品。子碼有以下幾種情況:
KERN_MEMORY_ERROR:試圖訪問當(dāng)時(shí)無法返回?cái)?shù)據(jù)的內(nèi)存妇垢,如內(nèi)存映射文件不可用。
EXC_ARM_DA_ALIGN:試圖訪問沒有正確對齊的內(nèi)存。此異常代碼很少見闯估,因?yàn)?4位ARM CPU可處理未對齊的數(shù)據(jù)灼舍。但是,如果內(nèi)存地址既未對齊又位于未映射的內(nèi)存區(qū)域中涨薪,則可能會(huì)看到此異常子類型骑素。
SIGILL
常見EXC_BAD_INSTRUCTION非法指令,通常與特定非法或未定義指令或操作數(shù)相關(guān)刚夺。
EXC_BREAKPOINT(SIGTRAP)
在ARM處理器上献丑,斷點(diǎn)異常類型指示跟蹤陷阱中斷進(jìn)程。 跟蹤陷阱使附上的調(diào)試器有機(jī)會(huì)在執(zhí)行特定位置時(shí)中斷該進(jìn)程侠姑。
斷點(diǎn)異常類型指示跟蹤陷阱中斷了該過程创橄。 跟蹤陷阱使附加的調(diào)試器有機(jī)會(huì)在執(zhí)行的特定點(diǎn)中斷該進(jìn)程。 在ARM處理器上莽红,它顯示為EXC_BREAKPOINT(SIGTRAP)妥畏。 在x86_64處理器上,它顯示為EXC_BAD_INSTRUCTION(SIGILL)安吁。
Swift運(yùn)行時(shí)將跟蹤陷阱用于特定類型的不可恢復(fù)的錯(cuò)誤-有關(guān)這些錯(cuò)誤的信息醉蚁,請參見Addressing Crashes from Swift Runtime Errors。 一些較低級(jí)別的庫(例如Dispatch)會(huì)在遇到不可恢復(fù)的錯(cuò)誤時(shí)使用此異常來捕獲進(jìn)程鬼店,并在崩潰報(bào)告的“其他診斷信息”部分中記錄有關(guān)該錯(cuò)誤的其他信息网棍。 有關(guān)這些消息的信息,請參閱Diagnostic Messages薪韩。
當(dāng)使用swift時(shí)确沸,以下幾種情況也會(huì)拋出此異常:
一個(gè)非可選類型值為nil;
強(qiáng)制類型轉(zhuǎn)換失敺荨罗捎;
如果要在自己的代碼中使用相同的技術(shù)來解決不可恢復(fù)的錯(cuò)誤,請調(diào)用__builtin_trap()函數(shù)拉盾。 這使系統(tǒng)可以生成帶有線程回溯的崩潰報(bào)告桨菜,以顯示你如何達(dá)到不可恢復(fù)的錯(cuò)誤。
- ILL_ILLTRP:ILL_ILLTRP at 0xxxxx通常是二進(jìn)制出錯(cuò)捉偏,典型比如app升級(jí)前后二進(jìn)制緩存出錯(cuò)倒得。
SIGFPE
崩潰的線程執(zhí)行了無效的算術(shù)運(yùn)算。
包括除以0或取余0的情況夭禽,及發(fā)生數(shù)據(jù)溢出導(dǎo)致的除以0或取余0的情況霞掺;包括浮點(diǎn)錯(cuò)誤。
The following values can be placed in si_code for a SIGFPE signal:
FPE_INTDIV Integer divide by zero.
FPE_INTOVF Integer overflow.
FPE_FLTDIV Floating-point divide by zero.
FPE_FLTOVF Floating-point overflow.
FPE_FLTUND Floating-point underflow.
FPE_FLTRES Floating-point inexact result.
FPE_FLTINV Floating-point invalid operation.
FPE_FLTSUB Subscript out of range.
SIGKILL
此信號(hào)表示系統(tǒng)中止進(jìn)程讹躯,通常是調(diào)用函數(shù)exit()或kill(9)產(chǎn)生菩彬。iOS中常見為受到系統(tǒng)資源限制而導(dǎo)致退出
崩潰報(bào)告會(huì)包含代表中止原因的編碼:
0x8badf00d:eate bad food缠劝,系統(tǒng)監(jiān)視程序由中止無響應(yīng)應(yīng)用。注意在生命周期的不同階段骗灶,觸發(fā)看門狗機(jī)制的超時(shí)時(shí)間是不一樣的惨恭。
0xc00010ff:cool off,系統(tǒng)由于過熱保護(hù)中止應(yīng)用耙旦,通常與特定的手機(jī)和環(huán)境有關(guān)脱羡。
0xdead10cc:dead lock,系統(tǒng)中止在掛起期間一直保持文件鎖或SQLite數(shù)據(jù)庫鎖的應(yīng)用免都。
0xbaadca11:bad all锉罐,系統(tǒng)由于應(yīng)用在響應(yīng)PushKit通知時(shí)無法報(bào)告CallKit呼叫而中止它。
0xbad22222:系統(tǒng)由于VoIP應(yīng)用恢復(fù)太頻繁而中止它绕娘。
0xc51bad01:OS終止了該應(yīng)用程序氓鄙,因?yàn)樗趫?zhí)行后臺(tái)任務(wù)時(shí)占用了過多的CPU時(shí)間。
0xc51bad02:OS終止了該應(yīng)用程序业舍,因?yàn)樗茨茉诜峙涞臅r(shí)間內(nèi)完成后臺(tái)任務(wù)抖拦。
0xc51bad03:OS終止了該應(yīng)用程序,因?yàn)樗茨茉诜峙涞臅r(shí)間內(nèi)完成后臺(tái)任務(wù)舷暮,但是系統(tǒng)總體上非常繁忙态罪,以至于該應(yīng)用程序可能沒有收到太多的CPU時(shí)間來執(zhí)行后臺(tái)任務(wù)。
0xbada5e47:系統(tǒng)可能由于你啟動(dòng)了過多了后臺(tái)任務(wù)而中止你的應(yīng)用下面。
iOS系統(tǒng)異常和限制
由于iOS通常運(yùn)行在移動(dòng)設(shè)備上复颈,為了保證移動(dòng)設(shè)備的使用體驗(yàn),iOS操作系統(tǒng)通常會(huì)設(shè)定各種限制
- UI線程無響應(yīng)
通常來自主線程阻塞沥割,操作系統(tǒng)為保障應(yīng)用程序的流暢會(huì)對主線程進(jìn)行watchdog監(jiān)聽耗啦,如果發(fā)現(xiàn)主線程在較長時(shí)間都沒有處理UI刷新或者事件響應(yīng),會(huì)觸發(fā)watchdog的kill機(jī)制机杜。引起主線程無響應(yīng)的情況有多種可能帜讲,常見場景有:
-
主線程死鎖
使用dispath_sync不規(guī)范導(dǎo)致死鎖
在主線程中執(zhí)行過長任務(wù)或者進(jìn)行死循環(huán)
主線程等待信號(hào)時(shí)間過長。
啟動(dòng)時(shí)間過長
啟動(dòng)時(shí)間過長通常也是由于主線程無響應(yīng)導(dǎo)致的椒拗,例子:
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d
Termination Description: SPRINGBOARD, scene-update watchdog transgression: application<com.soulapp.cn>:1952
exhausted real (wall clock) time allowance of 10.00 seconds | ProcessVisibility: Foreground | ProcessState: Running | WatchdogEvent: scene-update
| WatchdogVisibility: Foreground | WatchdogCPUStatistics:
( | "Elapsed total CPU time (seconds): 33.200 (user 33.200, system 0.000), 80% CPU", | "Elapsed application CPU time (seconds): 3.620, 9% CPU" | )
Triggered by Thread: 0
- 觸發(fā)系統(tǒng)資源限制
EXC_GUARD
受保護(hù)資源的非法訪問似将,一般是由違背受保護(hù)資源防護(hù)觸發(fā),例如非法訪問某些文件描述符蚀苛。
EXC_RESOURCE
資源受限在验,應(yīng)用由于達(dá)到資源的消耗限制而被退出,例如:
CPU使用過高
內(nèi)存使用過高
weakups過高
OC語言異常
OC運(yùn)行時(shí)也有一些自己特點(diǎn)的錯(cuò)誤堵未,通常表現(xiàn)為異常退出
異常 | 說明 | 觸發(fā)原因 |
---|---|---|
NSGenericException | 通用異常 | |
NSRangeException | 訪問越界異常 | 訪問數(shù)組腋舌,容器類越界 |
NSInvalidArgumentException | 非法參數(shù)異常 | 是 Objective -C 代碼最常出現(xiàn)的錯(cuò)誤,所以平時(shí)在寫代碼的時(shí)候渗蟹,需要多加注意块饺,加強(qiáng)對參數(shù)的檢查耻陕,避免傳入非法參數(shù)導(dǎo)致異常,其中尤以nil參數(shù)為甚刨沦。 |
NSInternalInconsistencyException | 內(nèi)部不一致異常 | 通常出現(xiàn)為對非mutable容器當(dāng)成mutable容器使用,也見于把Xib當(dāng)成Storyboard使用 |
NSMallocException | 分配內(nèi)存異常 | 通常是內(nèi)存不足引起的膘怕,如一次性申請過大內(nèi)存空間想诅,圖片占有內(nèi)存過大。iOS系統(tǒng)有時(shí)候也會(huì)限制在短時(shí)間內(nèi)頻繁申請和釋放內(nèi)存行為 |
NSObjectInaccessibleException | 對象不可訪問異常 | 常見于使用CoreData中對象被刪除或者不可用岛心,也出現(xiàn)于遠(yuǎn)程過程調(diào)用中對象不可訪問 |
NSObjectNotAvailableException | 對象不存在異常 | 訪問一些被限制的對象/方法来破,例如:在SwiftUI APP中使用alertView,遠(yuǎn)程過程調(diào)用對象不存在忘古,使用WebKitView可能會(huì)遇到 |
NSDestinationInvalidException | 目的不合法異常 | 常見于發(fā)起遠(yuǎn)程連接時(shí)地址不合法 |
NSPortTimeoutException | 通信超時(shí)異常 | 在建立網(wǎng)絡(luò)連接通信超時(shí) |
NSInvalidSendPortException | 發(fā)送端口已經(jīng)失效 | NSConnection斷開/失效時(shí)發(fā)送方收到異常 |
NSInvalidReceivePortException | 接收端口失效異常 | NSConnection斷開/失效時(shí)接收方收到異常 |
NSPortSendException | 端口發(fā)送異常 | NSConnection連接使用時(shí)可能遇到 |
NSPortReceiveException | 端口接收異常 | NSConnection連接使用時(shí)可能遇到 |
NSOldStyleException | 老式異常 | 已經(jīng)不在使用 |
NSInconsistentArchiveException | 初始化或者編碼異常 | 通常出現(xiàn)在文檔解析徘禁,存儲(chǔ)解析操作 |
此外還有一些自定義的異常,比如:
FileNotFoundException 文件讀寫時(shí)找不到異常
參考: