崩潰分析
崩潰日志(crash log)
根據(jù)符號表來監(jiān)測崩潰位置
-
什么是符號表
符號表就是指在Xcode項(xiàng)目編譯后,在編譯生成的二進(jìn)制文件.app的同級目錄下生成的同名的.dSYM文件捐名。
.dSYM文件其實(shí)是一個(gè)目錄力崇,在子目錄中包含了一個(gè)16進(jìn)制的保存函數(shù)地址映射信息的中轉(zhuǎn)文件袍榆,所有Debug的symbols都在這個(gè)文件中(包括文件名最岗、函數(shù)名烙常、行號等),所以也稱之為調(diào)試符號信息文件召耘。
-
符號表有什么用
符號表就是用來符號化 crash log(崩潰日志)百炬。crash log中有一些方法16進(jìn)制的內(nèi)存地址等,通過符號表就能找到對應(yīng)的能夠直觀看到的方法名之類污它。
-
如何得到.dsYM文件
我們在Archive的時(shí)候會生成.xcarchive文件剖踊,然后顯示包內(nèi)容就能夠在里面找到.dsYM文件和.app文件。
Xcode中查看崩潰信息
Xcode->Window->Organizer->Crashes
崩潰日志分析
參考iOS應(yīng)用崩潰日志分析里面有很詳細(xì)的分析介紹衫贬。
以上是一個(gè)完整的崩潰日志德澈。
如何得到崩潰日志
-
把設(shè)備連上電腦,得到自己設(shè)備的崩潰日志
崩潰日志可以從xcode里打開Devices看到對應(yīng)手機(jī)的一些崩潰信息固惯。點(diǎn)擊下圖的View Device Logs就能看到崩潰日志梆造。
-
使用第三方崩潰管理工具
我暫時(shí)只使用過友盟,友盟里面有錯(cuò)誤分析葬毫,就是截取的崩潰日志镇辉。
-
自己截取崩潰日志
自己寫入代碼,然后截取到崩潰日志供常,把崩潰日志發(fā)送到開發(fā)者郵箱里摊聋。 iOS Crash(崩潰)調(diào)試技巧這篇文章中有介紹如何截取崩潰日志并發(fā)送到郵箱。
分析崩潰日志
-
崩潰日志中的(3)異常
Exception Type: 異常類型 通常包含1.7中的Signal信號和EXC_BAD_ACCESS栈暇。
Exception Codes: 異常編碼 0x8badf00d: 讀做 “ate bad food”! (把數(shù)字換成字母,是不是很像 :p)該編碼表示應(yīng)用是因?yàn)榘l(fā)生watchdog超時(shí)而被iOS終止的箍镜。 通常是應(yīng)用花費(fèi)太多時(shí)間而無法啟動(dòng)源祈、終止或響應(yīng)用系統(tǒng)事件煎源。
0xbad22222: 該編碼表示 VoIP 應(yīng)用因?yàn)檫^于頻繁重啟而被終止。
0xdead10cc: 讀做 “dead lock”!該代碼表明應(yīng)用因?yàn)樵诤笈_運(yùn)行時(shí)占用系統(tǒng)資源香缺,如通訊錄數(shù)據(jù)庫不釋放而被終止 手销。
0xdeadfa11: 讀做 “dead fall”! 該代碼表示應(yīng)用是被用戶強(qiáng)制退出的。根據(jù)蘋果文檔, 強(qiáng)制退出發(fā)生在用戶長按開關(guān)按鈕直到出現(xiàn) “滑動(dòng)來關(guān)機(jī)”, 然后長按 Home按鈕图张。強(qiáng)制退出將產(chǎn)生 包含0xdeadfa11 異常編碼的崩潰日志, 因?yàn)榇蠖鄶?shù)是強(qiáng)制退出是因?yàn)閼?yīng)用阻塞了界面锋拖。
-
崩潰日志中的(4)線程回溯
這部分提供應(yīng)用中所有線程的回溯日志。 回溯是閃退發(fā)生時(shí)所有活動(dòng)幀清單祸轮。它包含閃退發(fā)生時(shí)調(diào)用函數(shù)的清單兽埃。看下面這行日志:
它包括四列: 幀編號—— 此處是2适袜。(數(shù)子從大到小為發(fā)生的順序) 二進(jìn)制庫的名稱 ——此處是 XYZLib. 調(diào)用方法的地址 ——此處是 0x34648e88. 第四列分為兩個(gè)子列柄错,一個(gè)基本地址和一個(gè)偏移量。此處是0×83000 + 8740, 第一個(gè)數(shù)字指向文件苦酱,第二個(gè)數(shù)字指向文件中的代碼行售貌。
野指針分析方法 ( Enable Malloc Scribble )
因?yàn)橐爸羔樀脑虬l(fā)生崩潰是常常出現(xiàn)的事,而且比較隨機(jī)疫萤。關(guān)于一些原因及概念后面我們會講到颂跨。所以我們要提高野指針的崩潰率好來幫我們快速找到有問題的代碼。
對象釋放后只有出現(xiàn)被隨機(jī)填入的數(shù)據(jù)是不可訪問的時(shí)候才會必現(xiàn)Crash扯饶。
這個(gè)地方我們可以做一下手腳恒削,把這一隨機(jī)的過程變成不隨機(jī)的過程。對象釋放后在內(nèi)存上填上不可訪問的數(shù)據(jù)帝际,其實(shí)這種技術(shù)其實(shí)一直都有蔓同,xcode的Enable Scribble就是這個(gè)作用。
更加詳細(xì)的介紹可以參考:如何定位Obj-C野指針隨機(jī)Crash蹲诀。
僵尸模式 ( NSZombieEnabled )
啟用了NSZombieEnabled的話斑粱,它會用一個(gè)僵尸來替換默認(rèn)的dealloc實(shí)現(xiàn),也就是在引用計(jì)數(shù)降到0時(shí)脯爪,該僵尸實(shí)現(xiàn)會將該對象轉(zhuǎn)換成僵尸對象则北。僵尸對象的作用是在你向它發(fā)送消息時(shí),它會顯示一段日志并自動(dòng)跳入調(diào)試器痕慢。
所以當(dāng)啟用NSZombieEnabled時(shí)尚揣,一個(gè)錯(cuò)誤的內(nèi)存訪問就會變成一條無法識別的消息發(fā)送給僵尸對象。僵尸對象會顯示接受到得信息掖举,然后跳入調(diào)試器快骗,這樣你就可以查看到底是哪里出了問題。
所以這時(shí)一般崩潰的原因是:調(diào)用了已經(jīng)釋放的內(nèi)存空間,或者說重復(fù)釋放了某個(gè)地址空間方篮。
如何找出問題
-
NSZombieEnabled
打開NSZombieEnabled之后名秀,如果遇到對應(yīng)的崩潰類型既調(diào)用了已經(jīng)釋放的內(nèi)存空間,或者說重復(fù)釋放了某個(gè)地址空間藕溅。那么就能在GDB中看到對應(yīng)的輸出信息匕得。
比如會出現(xiàn)如下這樣的問題:
[__NSArrayM addObject:]: message sent to deallocated instance 0x7179910
-
MallocStackLoggingNoCompact
如果崩潰是發(fā)生在當(dāng)前調(diào)用棧,通過上面的做法巾表,系統(tǒng)就會把崩潰原因定位到具體代碼中汁掠。但是,如果崩潰不在當(dāng)前調(diào)用棧集币,系統(tǒng)就僅僅只能把崩潰地址告訴我們考阱,而沒辦法定位到具體代碼,這樣我們也沒法去修改錯(cuò)誤惠猿。這時(shí)就可以修改scheme羔砾,讓xcode記錄每個(gè)地址alloc的歷史,這樣我們就可以用命令把這個(gè)地址還原出來偶妖。 如圖:(跟設(shè)置
NSZombieEnabled
一樣姜凄,添加MallocStackLoggingNoCompact
,并且設(shè)置為YES)這樣趾访,當(dāng)出現(xiàn)崩潰原因是message sent to deallocated instance 0x7179910态秧,我們可以使用以下命令,把內(nèi)存地址還原:
(gdb) nfo malloc-history 0x7179910
也可以使用下面的命令(gdb) shell malloc_history {pid/partial-process-name} {address}
這篇文章中有介紹MallocStackLoggingNoCompact的使用扼鞋。
-
總結(jié)
還有官方文檔 Enabling the Malloc Debugging Features 介紹了類似
NSZombieEnabled
和MallocStackLoggingNoCompact
這類的環(huán)境變量的作用申鱼。TODO:翻譯Enabling the Malloc Debugging Features這篇文章,寫對應(yīng)的demo測試這類變量設(shè)置后如何找出內(nèi)存出錯(cuò)問題云头。
Enable Address Sanitizer(地址消毒劑)
設(shè)置這個(gè)參數(shù)后就能看到一些更詳細(xì)的錯(cuò)誤信息提示捐友,甚至?xí)袃?nèi)存使用情況的展示。
C語言是一門危險(xiǎn)的語言溃槐,內(nèi)存安全是一個(gè)主要的問題匣砖。C語言中根本沒有內(nèi)存安全可言。像下面的代碼昏滴,會被正常的編譯猴鲫,而且可能正常運(yùn)行: char *ptr = malloc(5); ptr[12] = 0
; 對于內(nèi)存安全的驗(yàn)證已經(jīng)有一些解決方案了。如Clang的靜態(tài)代碼分析谣殊,可以從代碼中查找特定類型的內(nèi)存安全問題拂共。如Valgrind之類的程序可以在運(yùn)行時(shí)檢測到不安全的內(nèi)存訪問。
Address Sanitizer是另外一種解決方案姻几。它使用了一種新的方法宜狐,有利有弊势告。但仍不失為一個(gè)查找代碼問題的有力工具。
這類工具的理論依據(jù)是:訪問內(nèi)存時(shí)肌厨,通過比較訪問的內(nèi)存和程序?qū)嶋H分配的內(nèi)存培慌,驗(yàn)證內(nèi)存訪問的有效性豁陆,從而在bug發(fā)生時(shí)就檢測到它們柑爸,而不會等到副作用產(chǎn)生時(shí)才有所察覺。
malloc函數(shù)總是最少分配16個(gè)字節(jié)盒音。為了儲存針對標(biāo)準(zhǔn)malloc的內(nèi)存的保護(hù)表鳍,需要分配內(nèi)存到16字節(jié)的范圍內(nèi),因此祥诽,若分配的內(nèi)存大小不是16字節(jié)的整數(shù)倍譬圣,余出的幾個(gè)字節(jié)將不受保護(hù)。
Address Sanitizer會追蹤受限內(nèi)存雄坪,使用了一種簡單但是很巧妙的方法:它在進(jìn)程的內(nèi)存空間上保存了一個(gè)固定的區(qū)域厘熟,叫做“影子內(nèi)存區(qū)”。用內(nèi)存消毒劑的術(shù)語來說维哈,一個(gè)被標(biāo)記為受限的內(nèi)存被稱作“中毒”內(nèi)存绳姨。“影子內(nèi)存區(qū)”會記錄哪些內(nèi)存字節(jié)是中毒的阔挠。通過一個(gè)簡單的公式飘庄,可以將進(jìn)程中的內(nèi)存空間映射到“影子內(nèi)存區(qū)”中,即:每8字節(jié)的正常內(nèi)存塊映射到一個(gè)字節(jié)的影子內(nèi)存上购撼。在影子內(nèi)存上跪削,會跟蹤這8字節(jié)的“中毒狀態(tài)”。
Address Sanitizer 這篇文章詳細(xì)介紹了Enable Address Sanitizer迂求,對應(yīng)的中文翻譯在Xcode 7上直接使用Clang Address Sanitizer
Static Analyzer(靜態(tài)分析)
Static Analyzer是一個(gè)非常好的工具去發(fā)現(xiàn)編譯器警告不會提示的問題和一些個(gè)人的內(nèi)錯(cuò)泄露和死存儲(不會用到的賦了值的變量)錯(cuò)誤碾盐。這個(gè)方法可能大大的提高內(nèi)存使用和性能,以及提升應(yīng)用的整體穩(wěn)定性和代碼質(zhì)量揩局。
打開方式:Xcode->Product-Analyze 然后我們就能看到如下藍(lán)色箭頭所示的一些有問題的代碼毫玖。
unrecognized selector send to instancd 快速定位
在debug navigator的斷點(diǎn)欄里添加Create Symbolic Breakpoint。
在Symbolic中填寫如下方法簽名: -[NSObject(NSObject) doesNotRecognizeSelector:]
設(shè)置完成后再遇到類似的錯(cuò)誤就會定位到具體的代碼谐腰。
Signal和EXC_BAD_ACCESS錯(cuò)誤分析
Signal
在計(jì)算機(jī)科學(xué)中孕豹,信號(英語:Signals)是Unix、類Unix以及其他POSIX兼容的操作系統(tǒng)中進(jìn)程間通訊的一種有限制的方式十气。它是一種異步的通知機(jī)制励背,用來提醒進(jìn)程一個(gè)事件已經(jīng)發(fā)生。當(dāng)一個(gè)信號發(fā)送給一個(gè)進(jìn)程砸西,操作系統(tǒng)中斷了進(jìn)程正常的控制流程叶眉,此時(shí)址儒,任何非原子操作都將被中斷。如果進(jìn)程定義了信號的處理函數(shù)衅疙,那么它將被執(zhí)行莲趣,否則就執(zhí)行默認(rèn)的處理函數(shù)。
在iOS中就是未被捕獲的Objective-C異常(NSException)饱溢,導(dǎo)致程序向自身發(fā)送了SIGABRT信號而崩潰喧伞。
Signal信號的類型:
- SIGABRT–程序中止命令中止信號
- SIGALRM–程序超時(shí)信號
- SIGFPE–程序浮點(diǎn)異常信號
- SIGILL–程序非法指令信號
- SIGHUP–程序終端中止信號
- SIGINT–程序鍵盤中斷信號
- SIGKILL–程序結(jié)束接收中止信號
- SIGTERM–程序kill中止信號
- SIGSTOP–程序鍵盤中止信號
- SIGSEGV–程序無效內(nèi)存中止信號
- SIGBUS–程序內(nèi)存字節(jié)未對齊中止信號
- SIGPIPE–程序Socket發(fā)送失敗中止信號
iOS異常捕獲 這篇文章中有對各種信號的解釋。
-
SIGSEGV
SIGSEGV
是當(dāng)SEGV
發(fā)生的時(shí)候绩郎,讓代碼終止的標(biāo)識潘鲫。這是在iOS中最為常見導(dǎo)致崩潰的原因。當(dāng)App視圖去訪問沒有被開辟的內(nèi)存或者已經(jīng)被釋放的內(nèi)存時(shí)肋杖,這樣異常就會產(chǎn)生溉仑。調(diào)試這樣的Bug,需要找到已經(jīng)被釋放的變量,如果是在開發(fā)環(huán)境下發(fā)生這樣的問題状植,可以使用Zombies來檢測這樣的問題浊竟。
在低內(nèi)存的情況下,也可能發(fā)生類似的問題津畸。這樣非常難重現(xiàn)振定,在類和方法中跟蹤堆棧可能會找到原因洼畅。
舉個(gè)常見的例子吩案,在使用代理的時(shí)候:
// 代理應(yīng)該是用weak修飾的 self.delegate = myView; // myView從UINavigationController中Pop之后就會被銷毀,而self.delegate仍然起作用帝簇,成了野指針 // 將會拋出異常 [self.delegate doSomething];
避免這種異撑枪可以在調(diào)用之前檢查一下代理是否為空,是否能夠響應(yīng)所給的
Selector
if(self.delegate != nil) { if([self.delegate respondsToSelector:@selector(doSomething)]) { [self.delegate doSomething]; } }
-
NSInvalidArgumentException
通常根據(jù)堆棧信息能夠知道異常所發(fā)生的地方丧肴,最常見的引起
NSInvalidArgumentException
的原因就是an unrecognized selector
残揉,當(dāng)調(diào)用一個(gè)對象不存在方法是就會拋出。比如:
// 因?yàn)閷ο蟛荒茼憫?yīng)所調(diào)用的方法, 這就會拋出NSInvalidArgumentException異常 [@”myString” objectForKey:@”someKey”];
像上面這種明顯的錯(cuò)誤是編譯不過的芋浮,所以需要注意動(dòng)態(tài)參數(shù)抱环,比如序列化的
JSON
。// data => [‘item0’, ‘item1’, ‘item2’] NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; // JSON Data是個(gè)數(shù)組而不是一個(gè)字典 // 錯(cuò)誤在數(shù)組中調(diào)用了字典的方法 [json objectForKey:@”myKey”];
解決這樣的問題纸巷,可以在獲取對象前镇草,判斷一下是否響應(yīng)這個(gè)放否,數(shù)組是否包含了某個(gè)元素瘤旨。
-
SIGABRT
這是一個(gè)讓程序終止的標(biāo)識梯啤,當(dāng)存在一個(gè)未處理的異常的時(shí)候
debugger
顯示。在一個(gè)已經(jīng)發(fā)布的app中存哲,SIGABRT
會在斷言或者在app內(nèi)部因宇、操作系統(tǒng)滴啊用終止方法的時(shí)候拋出七婴。如果堆棧的信息是比較晦澀,看不懂察滑。很有可能是在蘋果
SDK
內(nèi)部終止了打厘。這樣的話,操作系統(tǒng)會殺死app并且拋出堆棧異常贺辰,這樣非常難調(diào)試户盯。注意:這并不一定意味著是系統(tǒng)的代碼存在Bug,代碼僅僅是成為了無效的狀態(tài),或者異常狀態(tài)魂爪。
SIGABRT
通常發(fā)生在異步執(zhí)行系統(tǒng)方法的時(shí)候先舷,所以一定小心,比如CoreData
滓侍,Accessing files
,NSUserDefaults
,還有一些其他的系統(tǒng)多線程操作。 -
SIGBUS
這個(gè)標(biāo)識意味著
BUS Error
牲芋,它和SIGABRT
很容易混淆撩笆。兩者都有相同點(diǎn),都是在訪問不可用內(nèi)存的時(shí)候發(fā)生缸浦,SIGBUS
是在訪問的地址不存在夕冲,或者無效的隊(duì)列。換句話說裂逐,SIGBUS就是物理地址無效(不同于SIGSEGV
歹鱼,SIGSEGV
是邏輯地址無效)SIGBUS
可能從大多數(shù)的同步方法中發(fā)出,當(dāng)試圖訪問一個(gè)鎖的時(shí)候可能會拋出這樣的異常 -
NSRangeException
當(dāng)代碼中試圖去訪問對象的范圍內(nèi)不存在的索引的時(shí)候卜高,根據(jù)堆棧信息可以追溯到這個(gè)異常弥姻,比如:
[__NSArrayM objectAtIndex:]: index 11 beyond bounds [0 .. 10]
大多數(shù)情況下引起這個(gè)問題的原因是數(shù)組和字符串,比如:
NSArray *arr = @[@1, @2, @3, @4]; NSNumber *num = [arr objectAtIndex:9]; // 拋出異常 NSString *mainString = @”myString”; NSString *subString = [mainString substringToIndex:24]; // 拋出異常
避免這種異常很簡單掺涛,及時(shí)確保索引在對象的范圍內(nèi)庭敦,比如:
NSNumber *obj = nil; NSArray *arr = @[@1, @2, @3, @4]; if([arr count] > 9) { obj = [arr objectAtIndex:9]; } NSString *subString = nil; NSString *mainString = @”myString”; if([mainString length] > 24) { subString = [mainString substringToIndex:24]; }
EXC_BAD_ACCESS
EXC_BAD_ACCESS是一個(gè)比較難處理的crash了,當(dāng)一個(gè)app進(jìn)入一種毀壞的狀態(tài)薪缆,通常是由于內(nèi)存管理問題而引起的時(shí)秧廉,就會出現(xiàn)出現(xiàn)這樣的crash。通常1.7.1中的Signal信號錯(cuò)誤都會提醒EXC_BAD_ACCESS拣帽。
文中1.3就介紹了用一些變量設(shè)置來找出這類錯(cuò)誤疼电。
崩潰類型收集
新老操作系統(tǒng)兼容
-
原因
開發(fā)人員在進(jìn)行開發(fā)的時(shí)候,常常使用的是某個(gè)操作系統(tǒng)版本减拭,所以在開發(fā)人員進(jìn)行開發(fā)測試的那個(gè)系統(tǒng)版本上基本不會出現(xiàn)問題蔽豺。但在其他版本上開發(fā)人員無法進(jìn)行完全的測試,這就導(dǎo)致了在新系統(tǒng)上運(yùn)行正常峡谊,但在舊系統(tǒng)上卻崩潰的情況茫虽。
在新 iOS 上正常的應(yīng)用刊苍,到了老版本 iOS 上秒退最常見原因是系統(tǒng)動(dòng)態(tài)鏈接庫或Framework無法找到。這種情況通常是由于 App 引用了一個(gè)新版操作系統(tǒng)里的動(dòng)態(tài)庫(或者某動(dòng)態(tài)庫的新版本)或只有新 iOS 支持的 Framework濒析,而又沒有對老系統(tǒng)進(jìn)行測試正什,于是當(dāng) App 運(yùn)行在老系統(tǒng)上時(shí)便由于找不到而秒退。
還有就是有些方法是新版操作系統(tǒng)才支持的号杏,而又沒有對該方法是否存在于老系統(tǒng)中做出判斷婴氮。這種情況其實(shí)還是比較難出現(xiàn)的,除非開發(fā)人員太low了盾致,因?yàn)檫@類方法在xcode編碼時(shí)編輯器都會有提醒的主经。
-
解決
這種問題一般就是用戶升級操作系統(tǒng)或者開發(fā)人員修改問題以兼容老系統(tǒng)。
本地存儲的數(shù)據(jù)結(jié)構(gòu)改變
-
原因
程序在升級時(shí)庭惜,修改了本地存儲的數(shù)據(jù)結(jié)構(gòu)罩驻,但是對用戶既存的舊數(shù)據(jù)沒有做好升級,結(jié)果導(dǎo)致初始化時(shí)因?yàn)闊o法正確讀取用戶數(shù)據(jù)而秒退护赊。
-
解決
第一種: 是把服務(wù)端傳過來的一些信息保存在本地惠遏,使用的時(shí)候從本地?cái)?shù)據(jù)庫取。
剛開始的時(shí)候我是第一次從服務(wù)端得到數(shù)據(jù)的時(shí)候直接解析成對應(yīng)的model然后存入plist文件里面骏啰。這時(shí)就有一個(gè)問題节吮,比如服務(wù)端新傳了字段newId,但是我舊版model里面沒有定義過判耕,存入本地的數(shù)據(jù)還是沒有這個(gè)字段透绩。
然后等我升級了程序,新程序里model壁熄,定義了這個(gè)newId字段帚豪,但是舊版里面數(shù)據(jù)已經(jīng)保存過一遍了沒有這個(gè)字段。這時(shí)再去取就取不到了请毛。
所以后來我就把存儲時(shí)解析數(shù)據(jù)改成了讀取時(shí)解析數(shù)據(jù)志鞍。就是不管服務(wù)端傳什么數(shù)據(jù)都把它存下來,然后在使用的時(shí)候再把它解析成對應(yīng)的model方仿,這樣就不會丟失字段了固棚。
第二種: 自己的一些數(shù)據(jù)存儲在本地SQLlite,新版的時(shí)候表結(jié)構(gòu)改了仙蚜。
SQLlite只支持更改一個(gè)表的名字此洲,或者向表中增加一個(gè)字段(列),但是我們不能刪除一個(gè)已經(jīng)存在的字段委粉,或者更改一個(gè)已經(jīng)存在的字段的名稱呜师、數(shù)據(jù)類型、限定符等等贾节。
這種就是有時(shí)候新版又添加字段了汁汗,或者改變了字段的名稱了衷畦。一般來說原有的字段名稱不應(yīng)該改變,但是添加新字段是常有的事知牌。
一般做法是在第一次創(chuàng)建表的時(shí)候加一些冗余字段祈争,以防后面不時(shí)之需。但是如果真沒辦法需要在舊表上增加新字段了角寸,那就要做數(shù)據(jù)遷移了菩混。
這里有一個(gè)庫在FMDB基礎(chǔ)上管理SQLlite數(shù)據(jù)庫了,可以用來做數(shù)據(jù)遷移用扁藕。FMDBMigrationManager
訪問的數(shù)據(jù)為空或訪問數(shù)據(jù)類型不對
-
原因
這類情況是比較常見的沮峡,后端傳回了空數(shù)據(jù),客戶端沒有做對應(yīng)的判斷繼續(xù)執(zhí)行下去了亿柑,這樣就產(chǎn)生了crash邢疙。或者自己本地的某個(gè)數(shù)據(jù)為空數(shù)據(jù)而去使用了橄杨。還有就是訪問的數(shù)據(jù)類型不是期望的數(shù)據(jù)類型而產(chǎn)生崩潰秘症。
-
解決
第一種: 服務(wù)端都加入默認(rèn)值,不返回空內(nèi)容或無key式矫,但是服務(wù)端往往會不太愿意改,還有就是有些確實(shí)應(yīng)該無值的話key也不用傳役耕,減少數(shù)據(jù)量的傳輸采转。
第二種: 這種就是客戶端自己做判斷,如果每次都是自己去if判斷是否為空或格式是否正確那肯定是比較麻煩的瞬痘。所以這里用到了NSArray和NSDictionary的Category故慈。一般我們訪問的數(shù)據(jù)都是NSArray或NSDictionary,所以在取值方法里面做一下判斷框全,返回正確的數(shù)據(jù)類型或默認(rèn)值即可察绷。
操作了不該操作的對象,野指針之類
野指針介紹
iOS中有空指針和野指針兩種概念津辩。
空指針是沒有存儲任何內(nèi)存地址的指針拆撼。如Student *s1 = NULL;
和Student *s2 = nil;
而野指針是指指向一個(gè)已刪除的對象(”垃圾”內(nèi)存既不可用內(nèi)存)或未申請?jiān)L問受限內(nèi)存區(qū)域的指針。野指針是比較危險(xiǎn)的喘沿。因?yàn)橐爸羔樦赶虻膶ο笠呀?jīng)被釋放了闸度,不能用了,你再給被釋放的對象發(fā)送消息就是違法的蚜印,所以會崩潰莺禁。
空指針和野指針這篇文章介紹了空指針和野指針的概念。
野指針崩潰情況
野指針訪問已經(jīng)釋放的對象crash其實(shí)不是必現(xiàn)的窄赋,因?yàn)閐ealloc執(zhí)行后只是告訴系統(tǒng)哟冬,這片內(nèi)存我不用了楼熄,而系統(tǒng)并沒有就讓這片內(nèi)存不能訪問。
所以野指針的崩潰是比較隨機(jī)的浩峡,你在測試的時(shí)候可能沒發(fā)生crash可岂,但是用戶在使用的時(shí)候就可能發(fā)生crash了。
現(xiàn)實(shí)出現(xiàn)問題大概是下面幾種可能的情況:
- 對象釋放后內(nèi)存沒被改動(dòng)過红符,原來的內(nèi)存保存完好青柄,可能不Crash或者出現(xiàn)邏輯錯(cuò)誤(隨機(jī)Crash)。
- 對象釋放后內(nèi)存沒被改動(dòng)過预侯,但是它自己析構(gòu)的時(shí)候已經(jīng)刪掉某些必要的東西致开,可能不Crash、Crash在訪問依賴的對象比如類成員上萎馅、出現(xiàn)邏輯錯(cuò)誤(隨機(jī)Crash)双戳。
- 對象釋放后內(nèi)存被改動(dòng)過,寫上了不可訪問的數(shù)據(jù)糜芳,直接就出錯(cuò)了很可能Crash在objc_msgSend上面(必現(xiàn)Crash飒货,常見)。
- 對象釋放后內(nèi)存被改動(dòng)過峭竣,寫上了可以訪問的數(shù)據(jù)塘辅,可能不Crash、出現(xiàn)邏輯錯(cuò)誤皆撩、間接訪問到不可訪問的數(shù)據(jù)(隨機(jī)Crash)扣墩。
- 對象釋放后內(nèi)存被改動(dòng)過,寫上了可以訪問的數(shù)據(jù)扛吞,但是再次訪問的時(shí)候執(zhí)行的代碼把別的數(shù)據(jù)寫壞了呻惕,遇到這種Crash只能哭了(隨機(jī)Crash,難度大滥比,概率低)Q谴唷!
- 對象釋放后再次release(幾乎是必現(xiàn)Crash盲泛,但也有例外濒持,很常見)。
參考下面這張圖:
內(nèi)存處理不當(dāng)
說到因?yàn)閮?nèi)存處理不當(dāng)崩潰就要涉及到內(nèi)存管理問題了查乒。內(nèi)存管理是軟件開發(fā)中一個(gè)重要的課題弥喉。iOS自從引入ARC機(jī)制后,對于內(nèi)存的管理開發(fā)者好像輕松了很多玛迄,但是還會發(fā)生一些內(nèi)存泄露之類的問題由境。
那我們?nèi)绾慰焖俚膩碚页鰞?nèi)存泄露呢?以前我們可能會使用Instruments來監(jiān)測,但是我們會發(fā)現(xiàn)使用Instruments特別繁瑣虏杰,而且不一定能定位到內(nèi)存泄露讥蟆。
所以這里偉大的Facebook工程師們開源了一些自動(dòng)化工具來解決監(jiān)測內(nèi)存泄露問題:FBRetainCycleDetector
、FBAllocationTracker
纺阔、FBMemoryProfiler
瘸彤。
原文介紹:Automatic memory leak detection on iOS
中文翻譯:在iOS上自動(dòng)檢測內(nèi)存泄露
主線程UI長時(shí)間卡死,被系統(tǒng)殺掉
主線程被卡住是非常常見的場景笛钝,具體表現(xiàn)就是程序不響應(yīng)任何的UI交互质况。這時(shí)按下調(diào)試的暫停按鈕,查看堆棧玻靡,就可以看到是到底是死鎖结榄、死循環(huán)等,導(dǎo)致UI線程被卡住囤捻。
這部分需要研究多線程臼朗,還有如何看調(diào)試欄里的線程的信息。
iOS調(diào)試?yán)锏倪M(jìn)程跟線程 這篇文章中有介紹多線程及死鎖的原因蝎土。
多線程之間切換訪問引起的crash
多線程引起的崩潰大部分是因?yàn)槭褂脭?shù)據(jù)庫的時(shí)候多線程同時(shí)讀寫數(shù)據(jù)庫而造成了crash视哑。 多線程導(dǎo)致的iOS閃退分析這篇文章就是關(guān)于多線程crash的調(diào)試。
多個(gè) Crash 日志收集服務(wù)共存的坑
在自己的程序里集成多個(gè) Crash 日志收集服務(wù)實(shí)在不是明智之舉誊涯。通常情況下挡毅,第三方功能性 SDK 都會集成一個(gè) Crash 收集服務(wù),以及時(shí)發(fā)現(xiàn)自己 SDK 的問題暴构。當(dāng)各家的服務(wù)都以保證自己的 Crash 統(tǒng)計(jì)正確完整為目的時(shí)慷嗜,難免出現(xiàn)時(shí)序手腳,強(qiáng)行覆蓋等等的惡意競爭丹壕,總會有人默默被坑。
-
如果同時(shí)有多方通過 NSSetUncaughtExceptionHandler 注冊異常處理程序薇溃,和平的作法是:
后注冊者通過 NSGetUncaughtExceptionHandler 將先前別人注冊的 handler 取出并備份菌赖,在自己 handler 處理完后自覺把別人的 handler 注冊回去,規(guī)規(guī)矩矩的傳遞沐序。不傳遞強(qiáng)行覆蓋的后果是琉用,在其之前注冊過的日志收集服務(wù)寫出的 Crash 日志就會因?yàn)槿〔坏?NSException 而丟失Last Exception Backtrace等信息。(P.S. iOS 系統(tǒng)自帶的 Crash Reporter 不受影響)
在開發(fā)測試階段策幼,可以利用 fishhook 框架去 hookNSSetUncaughtExceptionHandler方法邑时,這樣就可以清晰的看到 handler 的傳遞流程斷在哪里,快速定位污染環(huán)境者特姐。不推薦利用調(diào)試器添加符號斷點(diǎn)來檢查晶丘,原因是一些 Crash 收集框架在調(diào)試狀態(tài)下是不工作的。
-
檢測事例代碼:
static NSUncaughtExceptionHandler *g_vaildUncaughtExceptionHandler; static void (*ori_NSSetUncaughtExceptionHandler)( NSUncaughtExceptionHandler * ); void my_NSSetUncaughtExceptionHandler( NSUncaughtExceptionHandler * handler) { g_vaildUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(); if (g_vaildUncaughtExceptionHandler != NULL) { NSLog(@"UncaughtExceptionHandler=%p",g_vaildUncaughtExceptionHandler); } ori_NSSetUncaughtExceptionHandler(handler); NSLog(@"%@",[NSThread callStackSymbols]); g_vaildUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(); NSLog(@"UncaughtExceptionHandler=%p",g_vaildUncaughtExceptionHandler); }
文章寫得比較用心轉(zhuǎn)至: iOS 崩潰Crash解析