iOS開發(fā)中,解決Crash相信是開發(fā)者最為頭疼的問(wèn)題了,特別是對(duì)于已上線的應(yīng)用,對(duì)其Crash的跟蹤和修復(fù)顯得尤其重要,本文主要總結(jié)了常見的Crash類型以及主流的Crash日志收集及解析的解決方案。
- 本文主要目錄:
-
Objective-C Exception
** 常見的OC異常
** OC異常的抓取和分析 -
Mach Exception
** Unix Signal Exception -
Crash日志收集
** Crash日志收集方式
** Crash日志收集的沖突 -
堆棧符號(hào)解析
** Crash收集上報(bào)的堆棧信息解析
** 蘋果自帶Crash日志上報(bào)的堆棧信息解析 - 野指針Crash的解決方案
- 參考文章
Crash分為兩種,未捕獲的Objective-C異常和Mach異常。
一、Objective-C Exception
在OC層面(iOS庫(kù)、第三方庫(kù)出現(xiàn)錯(cuò)誤拋出)的異常稱為OC異常。
比如:
NSArray * array= @[@“s",@“x",@“m"];
[array objectAtIndex:4];
OC異常可以用try-catch抓住:
@try {
NSArray * array= @[@“s",@“x",@“m"];
[array objectAtIndex:4];
} @catch (NSException *exception) {
NSLog(@"%@",exception);
}
使用此方法可以抓到當(dāng)前拋出的異常并阻止程序崩潰,然而蘋果爸爸并不推薦這樣去做。
-[__NSArrayI objectAtIndex:]: index 4 beyond bounds [0 .. 2]
常見的OC異常
以下內(nèi)容來(lái)自文章《iOS開發(fā)質(zhì)量的那些事》
從上圖可以看到,iOS開發(fā)中常見的異常包括以下幾種:
NSInvalidArgumentException
NSRangeException
NSGenericException
NSInternalInconsistencyException
NSFileHandleOperationException
NSInvalidArgumentException
非法參數(shù)異常(NSInvalidArgumentException)是 Objective - C 代碼最常出現(xiàn)的錯(cuò)誤,所以平時(shí)在寫代碼的時(shí)候,需要多加注意,加強(qiáng)對(duì)參數(shù)的檢查,避免傳入非法參數(shù)導(dǎo)致異常,其中尤以nil參數(shù)為甚。
1. 集合數(shù)據(jù)的參數(shù)傳遞
比如NSMutableArray, NSMutableDictionary的數(shù)據(jù)操作
(1) NSDictionary不能刪除nil的key
(2) NSDictionary不能添加nil的對(duì)象
(3) 不能插入nil的對(duì)象
(4) 其他一些nil參數(shù)
2. 其他一些API的使用
APP一般都會(huì)有網(wǎng)絡(luò)操作,免不了使用網(wǎng)絡(luò)相關(guān)接口,比如NSURL的初始化,不能傳入nil的http地址:
3. 未實(shí)現(xiàn)的方法
(1) .h文件里函數(shù)名,卻忘了修改.m文件里對(duì)應(yīng)的函數(shù)名
(2) 使用第三方庫(kù)時(shí),沒(méi)有添加”-ObjC” flag
(3) MRC時(shí),大部分情況下是因?yàn)閷?duì)象被提前release了,在你心里不希望他release的情況下,指針還在,對(duì)象已經(jīng)不在 了。
NSRangeException
越界異常(NSRangeException)也是比較常出現(xiàn)的異常,有如下幾種類型:
1. 數(shù)組最大下標(biāo)處理錯(cuò)誤
比如數(shù)組長(zhǎng)度count, index的下標(biāo)范圍[0, count -1], 在開發(fā)時(shí),可能index的最大值超過(guò)數(shù)組的范圍;
2. 下標(biāo)的值是其他變量賦值
這樣會(huì)有很大的不確定性, 可能是一個(gè)很大的整數(shù)值
3. 使用空數(shù)組
如果一個(gè)數(shù)組剛剛初始化,還是空的,就對(duì)它進(jìn)行相關(guān)操作
所以,為了避免NSRangeException的發(fā)生,必須對(duì)傳入的index參數(shù)進(jìn)行合法性檢查,是否在集合數(shù)據(jù)的個(gè)數(shù)范圍內(nèi)。
NSGenericException
NSGenericException這個(gè)異常最容易出現(xiàn)在foreach操作中,在for in循環(huán)中如果修改所遍歷的數(shù)組,無(wú)論你是add或remove,都會(huì)出錯(cuò) "for in",它的內(nèi)部遍歷使用了類似 Iterator進(jìn)行迭代遍歷,一旦元素變動(dòng),之前的元素全部被失效,所以在foreach的循環(huán)當(dāng)中,最好不要去進(jìn)行元素的修改動(dòng)作,若需要修改,循環(huán)改為for遍歷,由于內(nèi)部機(jī)制不同,不會(huì)產(chǎn)生修改后結(jié)果失效的問(wèn)題。
NSInternalInconsistencyException
不一致導(dǎo)致出現(xiàn)的異常
比如NSDictionary當(dāng)做NSMutableDictionary來(lái)使用,從他們內(nèi)部的機(jī)理來(lái)說(shuō),就會(huì)產(chǎn)生一些錯(cuò)誤
NSMutableDictionary *info = method return to NSDictionary type;
[info setObject:@“sxm" forKey:@"name"];
比如xib界面使用或者約束設(shè)置不當(dāng)
NSFileHandleOperationException
處理文件時(shí)的一些異常,最常見的還是存儲(chǔ)空間不足的問(wèn)題,比如應(yīng)用頻繁的保存文檔,緩存資料或者處理比較大的數(shù)據(jù):
所以在文件處理里,需要考慮到手機(jī)存儲(chǔ)空間的問(wèn)題。
NSMallocException
這也是內(nèi)存不足的問(wèn)題,無(wú)法分配足夠的內(nèi)存空間
OC異常的抓取和分析
在debug環(huán)境下,OC異常導(dǎo)致崩潰時(shí)Xcode控制臺(tái)會(huì)輸出完整的異常信息,比如:
Terminating app due to uncaught exception ‘NSRangeException’, reason: ‘this is reason description’
,包括Exception的類型、原因和發(fā)生異常的完整堆棧。
這些信息一般來(lái)說(shuō)都足夠詳細(xì),足夠我們輕易地找到異常的位置并進(jìn)行修復(fù)。
非debug環(huán)境下,可以通過(guò)注冊(cè) NSUncaughtExceptionHandler 捕獲異常信息。雖然無(wú)法阻止APP崩潰,但是可以獲取異常信息并進(jìn)行收集,下次啟動(dòng)APP時(shí)進(jìn)行上報(bào),方便開發(fā)者進(jìn)行錯(cuò)誤跟蹤及修復(fù),這就是常用Crash收集工具所做的事情。
void InstallUncaughtExceptionHandler(void) {
NSSetUncaughtExceptionHandler(&handleUncaughtException);
}
void handleUncaughtException(NSException *exception) {
NSString * crashInfo = [NSString stringWithFormat:@"Exception name:%@\nException reason:%@\nException stack:%@",[exception name], [exception reason], [exception callStackSymbols]];
[WZCrashReporter saveCrash:crashInfo];
}
Mach Exception
Mach異常是指最底層的內(nèi)核級(jí)異常。
最常見的Mach異常:EXC_BAD_ACCESS (Bad Memory Access)
這種內(nèi)存訪問(wèn)異常分為訪問(wèn)非法地址(SIGBUS信號(hào))和訪問(wèn)了被回收掉的內(nèi)存(SIGSEGV信號(hào)),實(shí)際開發(fā)中遇到的錯(cuò)誤通常令人莫名其妙,往往需要大量時(shí)間來(lái)排查,非常頭疼。
EXC_BAD_ACCESS后面通常帶有code來(lái)幫助我們判斷到底是什么錯(cuò)誤,比如EXC_I386_GPFLT指訪問(wèn)了一塊已經(jīng)不屬于你的內(nèi)存。
一些其他的Mach異常:
- EXC_BAD_INSTRUCTION運(yùn)行了非法的指令,往往是運(yùn)行指令的參數(shù)不對(duì)(0或者nil的參數(shù))
- EXC_RESOURCE程序資源上限(cpu占用過(guò)高或者內(nèi)存不足)。
- EXC_GUARD一些C函數(shù)訪問(wèn)錯(cuò)誤導(dǎo)致的異常。
- 0x00000020奇怪異常集合,常見的是由于主線程阻塞看門狗殺死了APP《Exception Type: 00000020:什么是看門狗機(jī)制》
Unix Signal Exception
從Mach異常最終會(huì)轉(zhuǎn)化成Unix信號(hào)投遞到出錯(cuò)的線程(具體原理可以學(xué)習(xí)《漫談iOS Crash收集框架》, 各種信號(hào)的含義可以學(xué)習(xí)《iOS異常捕獲》。
- OC異常并不是真正的異常,但是當(dāng)一個(gè)OC異常被拋出到最外層還沒(méi)被捕獲,程序會(huì)強(qiáng)行發(fā)送SIGABRT信號(hào)中斷程序。
- Mach異常沒(méi)有比較便利的捕獲方式,既然它最終會(huì)轉(zhuǎn)化成信號(hào),我們也可以通過(guò)捕獲信號(hào),來(lái)捕獲 Crash 事件。
iOS提供了signal方法來(lái)注冊(cè)一個(gè)處理函數(shù),在處理函數(shù)中,使用execinfo中的 backtrace_symbols取出匯編層程序的堆棧信息。
代碼如下:
void InstallSignalHandler(void) {
signal(SIGHUP, handleSignalException);
signal(SIGINT, handleSignalException);
signal(SIGQUIT, handleSignalException);
signal(SIGABRT, handleSignalException);
signal(SIGILL, handleSignalException);
signal(SIGSEGV, handleSignalException);
signal(SIGFPE, handleSignalException);
signal(SIGBUS, handleSignalException);
signal(SIGPIPE, handleSignalException);
}
void handleSignalException(int signal) {
NSMutableString * crashInfo = [[NSMutableString alloc]init];
[crashInfo appendString:[NSString stringWithFormat:@"signal:%d\n",signal]];
[crashInfo appendString:@"Stack:\n"];
void* callstack[128];
int i, frames = backtrace(callstack, 128);
char** strs = backtrace_symbols(callstack, frames);
for (i = 0; i <frames; ++i) {
[crashInfo appendFormat:@"%s\n", strs[i]];
}
[WZCrashReporter saveCrash:crashInfo];
}
下面是一些常用信號(hào)代表的含義:
(1) SIGHUP
本信號(hào)在用戶終端連接(正常或非正常)結(jié)束時(shí)發(fā)出, 通常是在終端的控制進(jìn)程結(jié)束時(shí), 通知同一session內(nèi)的各個(gè)作業(yè), 這時(shí)它們與控制終端不再關(guān)聯(lián)。
登錄Linux時(shí),系統(tǒng)會(huì)分配給登錄用戶一個(gè)終端(Session)。在這個(gè)終端運(yùn)行的所有程序,包括前臺(tái)進(jìn)程組和后臺(tái)進(jìn)程組,一般都屬于這個(gè) Session。當(dāng)用戶退出Linux登錄時(shí),前臺(tái)進(jìn)程組和后臺(tái)有對(duì)終端輸出的進(jìn)程將會(huì)收到SIGHUP信號(hào)。這個(gè)信號(hào)的默認(rèn)操作為終止進(jìn)程,因此前臺(tái)進(jìn) 程組和后臺(tái)有終端輸出的進(jìn)程就會(huì)中止。不過(guò)可以捕獲這個(gè)信號(hào),比如wget能捕獲SIGHUP信號(hào),并忽略它,這樣就算退出了Linux登錄, wget也 能繼續(xù)下載。
此外,對(duì)于與終端脫離關(guān)系的守護(hù)進(jìn)程,這個(gè)信號(hào)用于通知它重新讀取配置文件。
(2) SIGINT
程序終止(interrupt)信號(hào), 在用戶鍵入INTR字符(通常是Ctrl-C)時(shí)發(fā)出,用于通知前臺(tái)進(jìn)程組終止進(jìn)程。
(3) SIGQUIT
和SIGINT類似, 但由QUIT字符(通常是Ctrl-)來(lái)控制. 進(jìn)程在因收到SIGQUIT退出時(shí)會(huì)產(chǎn)生core文件, 在這個(gè)意義上類似于一個(gè)程序錯(cuò)誤信號(hào)。
(6) SIGABRT
調(diào)用abort函數(shù)生成的信號(hào)。
(7) SIGBUS
非法地址, 包括內(nèi)存地址對(duì)齊(alignment)出錯(cuò)。比如訪問(wèn)一個(gè)四個(gè)字長(zhǎng)的整數(shù), 但其地址不是4的倍數(shù)。它與SIGSEGV的區(qū)別在于后者是由于對(duì)合法存儲(chǔ)地址的非法訪問(wèn)觸發(fā)的(如訪問(wèn)不屬于自己存儲(chǔ)空間或只讀存儲(chǔ)空間)。
(8) SIGFPE
在發(fā)生致命的算術(shù)運(yùn)算錯(cuò)誤時(shí)發(fā)出. 不僅包括浮點(diǎn)運(yùn)算錯(cuò)誤, 還包括溢出及除數(shù)為0等其它所有的算術(shù)的錯(cuò)誤。
(9) SIGKILL
用來(lái)立即結(jié)束程序的運(yùn)行. 本信號(hào)不能被阻塞、處理和忽略。如果管理員發(fā)現(xiàn)某個(gè)進(jìn)程終止不了,可嘗試發(fā)送這個(gè)信號(hào)。
(11) SIGSEGV
試圖訪問(wèn)未分配給自己的內(nèi)存, 或試圖往沒(méi)有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù).
(13) SIGPIPE
管道破裂。這個(gè)信號(hào)通常在進(jìn)程間通信產(chǎn)生,比如采用FIFO(管道)通信的兩個(gè)進(jìn)程,讀管道沒(méi)打開或者意外終止就往管道寫,寫進(jìn)程會(huì)收到SIGPIPE信號(hào)。此外用Socket通信的兩個(gè)進(jìn)程,寫進(jìn)程在寫Socket的時(shí)候,讀進(jìn)程已經(jīng)終止。
Crash日志收集
Crash日志收集方式
1.蘋果Crash收集服務(wù)
新版iTunes Connect已不能看到APP的crash日志,只能在XCode 中Window->Organizer->Crashes可以看到crash日志。
當(dāng)程序運(yùn)行Crash的時(shí)候,系統(tǒng)會(huì)把運(yùn)行的最后時(shí)刻的運(yùn)行信息記錄下來(lái),存儲(chǔ)到一個(gè)文件中,也就是我們所說(shuō)的Crash文件,但收集crash功能需要用戶設(shè)置->隱私->診斷與用量->診斷與用量數(shù)據(jù)選擇自動(dòng)發(fā)送,并與開發(fā)者共享,由于不是所有用戶都會(huì)把這個(gè)功能打開,所以并不能保證收集到所有的Crash信息,推薦指數(shù)三顆星。
2.自行實(shí)現(xiàn)Crash收集及上報(bào)框架
實(shí)現(xiàn)原理上面已詳細(xì)描述,適合人手充足,技術(shù)儲(chǔ)備足夠的團(tuán)隊(duì)使用,推薦指數(shù)五顆星。
3.第三方crash收集服務(wù)
騰訊bugly、友盟等Crash收集服務(wù)比較完善,作為開發(fā)者省心省力,適合個(gè)人或者對(duì)隱私性要求不高的團(tuán)隊(duì)使用,推薦指數(shù)五顆星。
Crash日志收集的沖突
以下內(nèi)容來(lái)自網(wǎng)絡(luò)文章
在我們自己研發(fā) Crash 收集框架之前,最早肯定都會(huì)接入騰訊 Bugly、友盟等第三方日志框架來(lái)進(jìn)行崩潰的收集和分析。如果多個(gè) Crash 收集框架存在時(shí),往往會(huì)存在沖突。
不管是對(duì)于 Signal 捕獲還是 NSException 捕獲都會(huì)存在 handler 覆蓋的問(wèn)題,正確的做法應(yīng)該是先判斷是否有前者已經(jīng)注冊(cè)了 handler,如果有則應(yīng)該把這個(gè) handler 保存下來(lái),在自己處理完自己的 handler 之后,再把這個(gè) handler 拋出去,供前面的注冊(cè)者處理。
typedef void (*SignalHandler)(int signo, siginfo_t *info, void *context);
static SignalHandler previousSignalHandler = NULL;
+ (void)installSignalHandler {
struct sigaction old_action;
sigaction(SIGABRT, NULL, &old_action);
if (old_action.sa_flags & SA_SIGINFO) {
previousSignalHandler = old_action.sa_sigaction;
}
LDAPMSignalRegister(SIGABRT);
// .......
}
static void LDAPMSignalRegister(int signal) {
struct sigaction action;
action.sa_sigaction = LDAPMSignalHandler;
action.sa_flags = SA_NODEFER | SA_SIGINFO;
sigemptyset(&action.sa_mask);
sigaction(signal, &action, 0);
}
static void LDAPMSignalHandler(int signal, siginfo_t* info, void* context) {
// 獲取堆棧,收集堆棧
........
LDAPMClearSignalRigister();
// 處理前者注冊(cè)的 handler
if (previousSignalHandler) {
previousSignalHandler(signal, info, context);
}
}
上面的是一個(gè)處理 Signal handler 沖突的大概代碼思路,下面是 NSException handler 的處理思路,兩者大同小異。
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler;
static void LDAPMUncaughtExceptionHandler(NSException *exception) {
// 獲取堆棧,收集堆棧
// ......
// 處理前者注冊(cè)的 handler
if (previousUncaughtExceptionHandler) {
previousUncaughtExceptionHandler(exception);
}
}
+ (void)installExceptionHandler {
previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&LDAPMUncaughtExceptionHandler);
}
堆棧符號(hào)解析
分為兩種情景討論:
1、Crash收集上報(bào)的堆棧信息解析
這種信息一般還原度比較高,基本上都能給出崩潰的具體定位信息,一些需要解析堆棧符號(hào)的解決方法如下:
以下內(nèi)容來(lái)自文章《iOS異常捕獲-堆棧信息的解析》
異常信息有三種類型:
1.已標(biāo)記錯(cuò)誤位置的:
test 0x000000010bfddd8c -[ViewController viewDidLoad] + 8588
- 這種信息已經(jīng)很明確了,不用解析
2.有模塊地址的情況:
test 0x00000001018157dc 0x100064000 + 24844252
以上面為例子,從左到右依次是:
二進(jìn)制庫(kù)名(test),調(diào)用方法的地址(0x00000001018157dc),模塊地址(0x100064000)+偏移地址(24844252)
3.無(wú)模塊地址的情況:
test 0x00000001018157dc test + 24844252
解析堆棧信息
dSYM符號(hào)表獲取,xcode->window->organizer->右鍵你的應(yīng)用 show finder->右鍵.xcarchive 顯示包內(nèi)容->dSYMs->test.app.dYSM
然后使用atos命令來(lái)符號(hào)化某個(gè)特定模塊加載地址
atos [-arch 架構(gòu)名] [-o 符號(hào)表] [-l 模塊地址] [方法地址]
使用終端,進(jìn)到test.app.dYSM所在目錄
一.如果是有模塊地址的情況,運(yùn)行:
atos -arch arm64 -o test.app.dSYM/Contents/Resources/DWARF/test -l 0x100064000 0x00000001018157dc
二.如果是無(wú)模塊地址的情況
1.先將偏移地址轉(zhuǎn)為16進(jìn)制:
24844252 = 0x17B17DC
2.然后用方法的地址-偏移地址,得到的就是模塊地址
0x00000001018157dc - 0x17B17DC = 0x100064000
3.最后運(yùn)行:
atos -arch arm64 -o test.app.dSYM/Contents/Resources/DWARF/test -l 0x100064000 0x00000001018157dc
2、蘋果自帶Crash日志上報(bào)的堆棧信息解析
以下內(nèi)容來(lái)自文章《iOS Crash 捕獲及堆棧符號(hào)化思路剖析》
有四種常見的方法:
* symbolicatecrash
* mac 下的 atos 工具
* linux 下的 atos 的替代品 atosl
* 通過(guò) dSYM 文件提取地址和符號(hào)的對(duì)應(yīng)關(guān)系,進(jìn)行符號(hào)還原
以上方案都有對(duì)應(yīng)的應(yīng)用場(chǎng)景,對(duì)于線上的 Crash 堆棧符號(hào)還原,主要采用的還是后三種方案。atos 和 atosl 的使用方法很類似,以下是 atos 的一個(gè)示例。
atos -o MonitorExample 0x0000000100062ac4 ARM-64 -l 0x100058000
// 還原結(jié)果
-[GYRootViewController tableView:cellForRowAtIndexPath:] (in GYMonitorExample) (GYRootViewController.m:41)
但是 atos 是Mac上一個(gè)工具,需要使用 Mac 或者黑蘋果來(lái)進(jìn)行解析工作,如果由后臺(tái)來(lái)做解析工作,往往需要一套基于 Linux 的解析方案,這個(gè)時(shí)候可以選擇 atosl,但是這個(gè)庫(kù)已經(jīng)有多年沒(méi)有更新了,同時(shí)基于我司的嘗試, atosl 好像不太支持 arm64 架構(gòu),所以我們放棄了該方案。
最終使用了第四個(gè)方案,提取 dSYM 的符號(hào)表,可以自己研發(fā)工具,也可以直接使用 bugly 和友盟的工具,下面是提取出來(lái)的符號(hào)表。第一列是起始內(nèi)存地址,第二列是結(jié)束地址,第三列是對(duì)應(yīng)的函數(shù)名、文件名以及行號(hào)。
a840 a854 -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:41
a854 a858 -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
a858 a87c -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
a87c a894 -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
a894 a8a0 -[GYRootViewController tableView:cellForRowAtIndexPath:] GYRootViewController.m:42
aa3c aa80 -[GYFilePreviewViewController initWithFilePath:] GYRootViewController.m:21
aa80 aaa8 -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:23
aaa8 aab8 -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:23
aab8 aabc -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:24
aabc aac8 -[GYFilePreviewViewController initWithFilePath:] GYFilePreviewViewController.m:24
因?yàn)槌绦蛎看螁?dòng)基地址都會(huì)變化,所以上面提到的地址是相對(duì)偏移地址,在我們獲取到崩潰堆棧地址后,可以根據(jù)堆棧中的偏移地址來(lái)與符號(hào)表中的地址來(lái)做匹配,進(jìn)而找到堆棧所對(duì)應(yīng)的函數(shù)符號(hào)。比如下面的第四行,偏移為 43072 轉(zhuǎn)換為十六進(jìn)制就是 a840,用 a840 去上面的符號(hào)表中找對(duì)應(yīng)關(guān)系,會(huì)發(fā)現(xiàn)對(duì)應(yīng)著 -[GYRootViewController tableView:cellForRowAtIndexPath:],基于這種方式,就可以將堆棧地址完全還原為函數(shù)符號(hào)啦。
0 libsystem_kernel.dylib 0x0000000186cfd314 0x186cde000 + 127764
1 Foundation 0x00000001887f5590 0x1886ec000 + 1086864
2 GYMonitorExample 0x00000001000da4ac 0x1000d0000 + 42156
3 GYMonitorExample 0x00000001000da840 0x1000d0000 + 43072
野指針Crash的解決方案
野指針是最玄的Crash,表現(xiàn)在飄忽不定(往往不是100%復(fù)現(xiàn))、難以定位(往往崩潰時(shí)的堆棧信息并不能定位到具體的代碼行)、難以消除(往往負(fù)責(zé)的業(yè)務(wù)和代碼邏輯使其隱藏得很深,在測(cè)試的覆蓋范圍外出現(xiàn))。
既然不能根治,那就盡量把它抑制吧,下面是一些優(yōu)秀的文章:
- 如何定位Obj-C野指針隨機(jī)Crash(一):先提高野指針Crash率
- 如何定位Obj-C野指針隨機(jī)Crash(二):讓非必現(xiàn)Crash變成必現(xiàn)
- 如何定位Obj-C野指針隨機(jī)Crash(三):加點(diǎn)黑科技讓Crash自報(bào)家門
- 淺談iOS Crash
參考文章:
1、iOS異常捕獲
2、iOS Crash文件的解析
3、漫談iOS Crash收集框架
4、iOS開發(fā)質(zhì)量的那些事