一個APP的崩潰率是對一個前端人員是否為合格的一個審核標準.
那么在每天日常中開發(fā)迭代中的那么多崩潰率信息有哪些呢?
又是因為什么原因?qū)е碌哪?該怎么去避免?
APP常見崩潰:
- Container crash(數(shù)組越界,插nil等)
- 字典的構(gòu)造與修改
- 操作 UITableView UICollectionView 數(shù)據(jù)增刪改讀操作
- NSString crash (字符串操作的crash)
- unrecognized selector
- UI not on Main Thread Crash (非主線程刷UI (機制待改善))
1.數(shù)組下標的越界
簡易示例代碼:
- (void)testArrayOut {
NSArray *array = @[@"a", @"b", @"c"];
NSLog(@"%@", array[4]);
}
崩潰信息
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 4 beyond bounds [0 .. 2]'
一般對數(shù)組的操作出現(xiàn)Crash的情況:
* 取值:index超出array的索引范圍
* 添加:新增的object為nil或者Null
* 插入:index大于count济赎、插入的object為nil或者Null
* 刪除:index超出array的索引范圍
* 替換:index超出array的索引范圍酬蹋、替換的object為nil或者Null
2.字典的構(gòu)造與修改
簡易示例代碼:
- (void)testDictionCrash {
NSString *aKey = nil;
NSDictionary *dictionary = @{@"a": aKey};
NSLog(@"%@", dictionary);
}
一般對字典操作出現(xiàn)Crash的情況:
- NSDictionary 不支持 nil 作為 key.
- NSDictionary 不支持 nil 作為 value.
崩潰信息
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]'
以上預防方法:
- 替換系統(tǒng)方法.通過 Method Swizzling 替換成自己的方法,然后再執(zhí)行方法的時候加以判斷.
- 使用類別擴展一個方法自定義實現(xiàn)對應操作后的預防.
- 萬能基佬王:http://t.cn/R0CTokN
- 萬能谷歌法:http://t.cn/R0CTNZE
3.操作 UITableView UICollectionView 數(shù)據(jù)增刪改讀操作
簡易示例代碼:
- (void)testTableViewUpdateCrash {
NSIndexPath *insertIndexPath = [NSIndexPath indexPathForRow:10 inSection:0];
NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForRow:11 inSection:0];
NSIndexPath *reloadIndexPath = [NSIndexPath indexPathForRow:12 inSection:0];
NSIndexPath *moved1IndexPath = [NSIndexPath indexPathForRow:13 inSection:0];
NSIndexPath *moved2IndexPath = [NSIndexPath indexPathForRow:14 inSection:0];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:@[insertIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView deleteRowsAtIndexPaths:@[deleteIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView reloadRowsAtIndexPaths:@[reloadIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView moveRowAtIndexPath:moved1IndexPath toIndexPath:moved2IndexPath];
[self.tableView endUpdates];
}
崩潰信息
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete row 12 from section 0 which only contains 10 rows before the update'
一般出現(xiàn)Crash情況:
- 1.返回的數(shù)據(jù)源 和 insert / delete 后,section所包含的行數(shù)不一致.
- 2.刷新對應行數(shù)的UI沒有對應上數(shù)據(jù)源.
預防方法:
- 1.當需要動態(tài)更新tableView的數(shù)據(jù)時惋嚎,計算好模型的數(shù)據(jù)使模型的數(shù)據(jù)和更新tableView的后的數(shù)據(jù)保持同步。
- 2.在調(diào)用deleteRowsAtIndexPaths:方法前,要確保數(shù)據(jù)為最新希痴。也就是說,先將要刪除的數(shù)據(jù)從數(shù)據(jù)源中刪除春感。
- 3.分組和分組中行數(shù)是變動的砌创,不能寫死.
- 4.先操作數(shù)據(jù)源,再操作UITableView對應的刷新動畫
4.NSAttributedString,NSMutableString 等可變對象操作相關
簡易示例代碼
- (void)testMutableAttributedStringCrash {
NSString *string = nil;
NSMutableString *mutableString = [[NSMutableString alloc] initWithString:string];
NSLog(@"%@",mutableString);
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string];
NSLog(@"%@",attributedString);
}
崩潰信息
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSConcreteMutableAttributedString initWithString:: nil value'
一般出現(xiàn)Crash情況:
1.初始化一個空的String.
2.拼接,插入,替換等操作一個nil的值.
預防方法:
- 檢查參數(shù)和參數(shù)類型的判斷.
5.unrecognized selector
簡易示例代碼:
- (void)testUnrecognizedSelectorCash {
[self performSelector:@selector(testSelCrash) withObject:nil afterDelay:0];
}
崩潰信息
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController testSelCrash]: unrecognized selector sent to instance 0x7f93a1e03e80'
一般出現(xiàn)Crash的情況:
- 對象被提前釋放,指針變成野指針.
- 對象本身就是野指針.如聲明一個局部對象鲫懒,沒有初始化就直接調(diào)用.
- 一個正常的對象調(diào)用一個不存在的方法.
- 給實體對象發(fā)送了不認識的消息嫩实,即對象調(diào)用方法出錯.
預防方法:
runtime中具體的方法調(diào)用流程大致如下:
1.首先,在相應操作的對象中的緩存方法列表中找調(diào)用的方法窥岩,如果找到甲献,轉(zhuǎn)向相應實現(xiàn)并執(zhí)行。
2.如果沒找到颂翼,在相應操作的對象中的方法列表中找調(diào)用的方法晃洒,如果找到,轉(zhuǎn)向相應實現(xiàn)執(zhí)行
3.如果沒找到朦乏,去父類指針所指向的對象中執(zhí)行1球及,2步驟.
4.以此類推,如果一直到根類還沒找到呻疹,轉(zhuǎn)向攔截調(diào)用吃引,走消息轉(zhuǎn)發(fā)機制。
5.如果沒有重寫攔截調(diào)用的方法,程序報錯际歼。
由上圖可見惶翻,在一個函數(shù)找不到時,runtime提供了三種方式去補救:
1鹅心、調(diào)用resolveInstanceMethod給機會讓類添加這個實現(xiàn)這個函數(shù)
2吕粗、調(diào)用forwardingTargetForSelector讓別的對象去執(zhí)行這個函數(shù)
3、調(diào)用forwardInvocation(函數(shù)執(zhí)行器)靈活的將目標函數(shù)以其他形式執(zhí)行旭愧。
如果都不中颅筋,調(diào)用doesNotRecognizeSelector拋出異常。
通過實現(xiàn)NSObject的forwardingTargetForSelector:方法.并利用class_addMethod方法動態(tài)添加函數(shù).
選擇原因:
1.resolveInstanceMethod 需要在類的本身上動態(tài)添加它本身不存在的方法输枯,這些方法對于該類本身來說冗余的
- forwardInvocation可以通過NSInvocation的形式將消息轉(zhuǎn)發(fā)給多個對象议泵,但是其開銷較大,需要創(chuàng)建新的NSInvocation對象桃熄,并且forwardInvocation的函數(shù)經(jīng)常被使用者調(diào)用先口,來做多層消息轉(zhuǎn)發(fā)選擇機制,不適合多次重寫
- forwardingTargetForSelector可以將消息轉(zhuǎn)發(fā)給一個對象瞳收,開銷較小碉京,并且被重寫的概率較低,適合重寫
可參考優(yōu)秀文獻:
6.非主線程刷UI
在非主線程刷UI將會導致app運行crash,現(xiàn)在Xcode在編譯的時候會檢測出當前有子線程操作UI有對應====的提示.但是其實也有必要對其進行處理螟深。
預防方案處理
- 詳細見Demo
以上為比較常見的簡單崩潰和一些簡單的預防方法,還有一些難以重現(xiàn)的崩潰.如野指針.內(nèi)存泄露導致的崩潰等問題.
APP常見不好跟蹤的異常崩潰:
- SIGSEGV || EXC_BAD_ACCESS || SIGBUS 野指針
- SIGPIPE 異常
- EXC_CRASH || SIGABRT 異常退出
- 內(nèi)存泄露
1.EXC_BAD_ACCESS || SIGSEGV || SIGBUS 野指針
出現(xiàn)原因:
試圖訪問未分配給自己的內(nèi)存,或試圖往沒有寫權(quán)限的內(nèi)存地址寫數(shù)據(jù).另外,在低內(nèi)存的時候,也可能會產(chǎn)生這樣的異常.
預防方法:
- 提高野指針Crash率.參考文獻:
如何定位Obj-C野指針隨機Crash(一):http://t.cn/R0NZLTU
如何定位Obj-C野指針隨機Crash(二):http://t.cn/R0NZbXM
如何定位Obj-C野指針隨機Crash(三):http://t.cn/R0NZqWM
2.SIGPIPE 異常
出現(xiàn)原因:
對一個端已經(jīng)關閉的socket調(diào)用兩次寫入操作谐宙,第二次寫入將會產(chǎn)生SIGPIPE信號,該信號默認結(jié)束進程界弧。
預防方法:
// 僅在 IOS 系統(tǒng)上支持 SO_NOSIGPIPE
#if defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL)
// We do not want SIGPIPE if writing to socket.
const int value = 1;
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(int));
#endif
將改代碼放在PCH文件中即可.
可參考文獻:
1.http://t.cn/R0Cs9T0 (維基百科)
2.http://t.cn/R0CselE
3.http://t.cn/R0NvcIx
3.EXC_CRASH || SIGABRT 異常退出
出現(xiàn)原因:
程序異常退出,導致這類異常崩潰的原因是捕獲到Objective-C/C++異常和調(diào)用了abort()函數(shù),會在斷言/app內(nèi)部/操作系統(tǒng)用終止方法拋出.通常發(fā)生在異步執(zhí)行系統(tǒng)方法的時候,如CoreData/NSUserDefaults等,還有一些其他的系統(tǒng)多線程操作.這并不一定意味著是系統(tǒng)代碼存在bug,代碼僅僅是成了無效狀態(tài),或者異常狀態(tài).
預防方法:
如果他們需要太多的時間來初始化凡蜻,程序?qū)⒈唤K止,因為觸發(fā)了看門狗垢箕。如果是因為啟動的時候被掛起划栓,所產(chǎn)生的崩潰報告異常類型(Exception Subtype)將是launch_hang。因為擴展(extensions)并沒有一個main函數(shù)条获,任何花銷在初始化的時間都發(fā)生在靜態(tài)構(gòu)造函數(shù)(static constructors)和呈現(xiàn)在你的擴展(extensions)和依賴庫的+load方法茅姜。你應該盡可能的延遲做這些工作。
4.內(nèi)存泄露
出現(xiàn)原因:
程序運行時一直分配內(nèi)存而不及時釋放無用的內(nèi)存月匣,程序占用的內(nèi)存越來越大钻洒,直到把系統(tǒng)分配給該APP的內(nèi)存消耗殫盡,程序因無內(nèi)存可用導致崩潰锄开,這樣的情況我們稱之為內(nèi)存泄漏素标。
預防方法:
1.使用Xcode自帶的 Instruments 內(nèi)存分析工具(Leaks)
2.使用WeRead團隊提供的自動化工具來監(jiān)測內(nèi)存泄露問題
pod 'MLeaksFinder'
pod 'FBRetainCycleDetector
原理:為基類 NSObject 添加一個方法 -willDealloc 方法,該方法的作用是萍悴,先用一個弱指針指向 self头遭,并在一小段時間(3秒)后寓免,通過這個弱指針直接調(diào)用斷言.
可參考文獻:
MLeaksFinder:精準 iOS 內(nèi)存泄露檢測工具: http://t.cn/RGC7Gdg
結(jié)束語
該文的介紹不包含所有崩潰,只是拋磚引玉與大家一起討論學習討論.
為了少讓我們少受Bug的摧殘,還提心吊膽的擔心線上某個崩潰問題引起大的影響计维。希望我們輕松工作袜香,快樂生活,早日擺脫bug煩惱鲫惶。