原文
隨著公司嘟嘟牛app用戶數量多了起來,崩潰的問題也多了起來,最近這幾天終於得空,集中時間處理了一下崩潰的問題,現總結一下,希望對大家有所幫助。
殺手 NO.1:NSInvalidArgumentException 異常
出現這個crash的原因有很多,選取了崩潰次數較多的crash。
crash 日志1-1
-[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[3]
crash日志拿到了,怎麼復現該現象呢?我們看到initWithObjects:forKeys:count:,猜測一下應該是NSDictionary初始化時的問題,在看後面的提示attempt to insert nil object,此時就可以做一個猜測,應該是NSDictionary初始化時插入nil對象造成的異常。下面我們寫一段代碼來驗證一下:
NSString *password = nil; NSDictionary *dict = @{ @"userName": @"bruce", @"password": password }; NSLog(@"dict is : %@", dict);
運行過後,崩潰信息如下:
Crash 日志1-1
上面的崩潰信息證明了我們的猜測。從崩潰日志記錄中,查詢到該問題的崩潰記錄有33條(總崩潰記錄304條),占10.85%,崩潰率比較高。為什麼會出現這種現象呢?如何解決這樣的crash呢?
崩潰率高的原因是因為自己的框架中采用了去model化的設計思想,不會把後台返回的數據轉換成model,而是通過一個reformer機制轉換成NSDictionary形式,提供給目標對象使用,在轉換成NSDictionary的過程中,後台返回的數據有時可能為空,就會造成插入nil對象,從而導致crash。
有3種方案可以解決該問題,如下:
方案一:後台在返回數據的時候進行校驗,對空值進行處理。但是在項目中有些空值是有特殊的用途,此種方案不可行。
方案二:在轉換成NSDictionary的時候,對後台返回的數據進行校驗,把空值轉換成NSNull對象。方案可行,但是需要對現有代碼做大的改動,每次轉換的時候都需要進行校驗,太麻煩。業務高速發展時期,這樣做成本太高。
方案三:有沒有一種無須改動現有代碼又能解決該問題呢?答案是有的,可以利用Objective-C的runtime來解決該問題。
NSDictionary插入nil對象會造成崩潰,但是插入NSNull對象是不會造成崩潰的,只要利用runtime的Swizzle Method把nil對象給轉換成NSNull對象就可以把該問題給解決了。創建一個NSDictionary的類別,利用runtime的Swizzle Method來替換系統的方法。源碼實現可以參考Glow團隊封裝的NSDictionary+NilSafe(Github上可下載到), 全部源碼會在文章末尾提供,現截取其中的部分代碼如下:
+ (instancetype)gl_dictionaryWithObjects:(const id [])objects forKeys:(const id(NSCopying)(因識別問題,此處圓括號替換尖括號)[])keys count:(NSUInteger)cnt { id safeObjects[cnt]; id safeKeys[cnt]; NSUInteger j = 0; for (NSUInteger i = 0; i < cnt; i++) { id key = keys[i]; id obj = objects[i]; if (!key) { continue; } if (!obj) { obj = [NSNull null]; } safeKeys[j] = key; safeObjects[j] = obj; j++; } return [self gl_dictionaryWithObjects:safeObjects forKeys:safeKeys count:j]; }
crash 日志1-2
data parameter is nil
通過日志信息,可以把崩潰問題定位到參數為nil的情況,在看了下堆棧的日志信息,把問題定位到了NSJSONSerialization序列化的時候,傳入data為nil,造成的崩潰。為了驗證是不是該問題,我寫了一段代碼做了下驗證:
NSData *data = nil; NSError *error; NSDictionary *orginDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]; NSLog(@"originDict is : %@", orginDict);
運行後,崩潰信息如下:
Crash日志 1-2
這個問題比較好解決,在序列化的時候,統一加入判斷,判斷data是不是nil即可。
crash 日志1-3
unrecognized selector sent to instance 0x15d23910
造成這條崩潰的原因,想必大家都比較熟悉了,就是一個類調用了一個不存在的方法,造成的崩潰。解決這樣的問題,可以在寫一個方法的時候,判斷一下其類的類型,不符合類型的不讓其調用,也可以使用runtime對常見的方法調用做一下錯誤兼容。比如我這邊經常會出現這樣的崩潰:
-[__NSCFConstantString objectForKeyedSubscript:]: unrecognized selector sent to instance 0x1741af420 -[NSNull length]: unrecognized selector sent to instance 0x1b21e6ef8 -[__NSCFConstantString objectForKeyedSubscript:]: unrecognized selector sent to instance -[__NSDictionaryI length]: unrecognized selector sent to instance 0x174264500
當這些對象調用這幾個不存在的方法的時候,替換成自己定義的一個方法,對它們做一下錯誤兼容,使應用不會崩潰。現截取部分代碼實現,全部源碼會在文章末尾提供。
@implementation NSString (NSRangeException) + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @autoreleasepool { [objc_getClass("__NSCFConstantString") swizzleMethod:@selector(objectForKeyedSubscript:) swizzledSelector:@selector(replace_objectForKeyedSubscript:)]; } }); } - (id)replace_objectForKeyedSubscript:(NSString *)key { return nil; } @end
小結一下,造成NSInvalidArgumentException異常大概有以下原因:
NSDictionary插入nil的對象。NSMutableDictionary也是同樣的道理。
NSJSONSerialization序列化的時候,傳入data為nil。
an unrecognized selector 無法識別的方法
NSInvalidArgumentException的崩潰記錄有149條(總崩潰記錄304條),占49.01%,稱霸Crash界,殺手排名第一。
殺手 NO.2:SIGSEGV 異常
SIGSEGV是當SEGV發生的時候,讓代碼終止的標識。當去訪問沒有被開辟的內存或者已經被釋放的內存時,就會發生這樣的異常。另外,在低內存的時候,也可能會產生這樣的異常。
對於這樣的異常,我們可以使用兩種方式來解決,一種方式使用Xcode自帶的內存分析工具(Leaks),一種是使用facebook提供的自動化工具來監測內存洩漏問題,如:
FBRetainCycleDetector、FBAllocationTracker、FBMemoryProfiler
例子1:
dataOut = malloc(dataOutAvailable * sizeof(uint8_t));
這是使用Xcode自帶的Leaks工具檢測到的內存洩漏,通過代碼我們看出這是一個C語言使用malloc函數分配了一塊內存地址,但是在不使用的時候卻忘記了釋放其內存地址,這樣就造成了內存洩漏,應該在其不使用的時候加上如下代碼:
free(dataOut);
另外,通過這個例子我們也要特別注意,在使用C語言對象的時候,一定要記得在不使用的時候給釋放掉,ARC並不能釋放掉這塊內存。
例子2:
Can't add self as subview crash
造成這個崩潰的原因,一種原因是在push或pop一個視圖的時候,並且設置了animated:YES,如果此時動畫(animated)還沒有完成,這個時候,你在去push或pop另外一個視圖的時候,就會造成該異常。 也有其他原因可以造成這個崩潰,比如:
[self.view addSubview:self.view];
復現這個現象,我寫了一個下面的代碼測試,如下:
- (IBAction)btnAction:(id)sender { UIViewController *test01 = [[UIViewController alloc] init]; [self.navigationController pushViewController:test01 animated:YES]; [self.navigationController pushViewController:test01 animated:YES]; }
解決該異常最簡單的方式是把animated設置為NO,但是很不友好,把系統自帶的動畫效果給去掉了。另外一種友好的方式就是通過runtime來進行實現了,通過安全的方式,確保當有控制器正在進行入棧或出棧時,沒有其他入棧或出棧操作。具體源碼會在文章末尾提供。
SIGSEGV的崩潰記錄有57條(總共304條崩潰記錄),占18.75%。在Crash界排名第二。
殺手 NO.3:NSRangeException 異常
造成這個異常,就是越界異常了,在iOS中我們經常碰到的越界異常有兩種,一種是數組越界,一種字符串截取越界,我們通過crash日志來具體分析一下。
crash 日志3-1
-[__NSArrayM objectAtIndex:]: index 1 beyond bounds for empty array -[__NSCFConstantString substringToIndex:]: Index 10 out of bounds; string length 0
通過日志可以很明顯的知道問題,就是越界造成的,復現該現象也比較簡單,在此就略過了。怎麼解決呢?
方案一:在對數組取數據的時候,要判斷一下數組的長度大於取的index,這個要在平時寫代碼的時候給規范起來。同樣在對字符串進行截取的時候,也需要做類似的判斷。但現實的情況是,有時我們會忘了寫這樣的邏輯判斷,就會有潛在的崩潰問題。如何做一下統一的判斷呢?即使開發人員忘了寫這樣的邏輯判斷也不會造成崩潰,從框架層面來杜絕這類的崩潰,方案二給出了答案。
方案二:利用runtime的Swizzle Method特性,可以實現從框架層面杜絕這類的崩潰問題,這樣做的好處有兩點:
開發人員忘了寫判斷越界的邏輯,也不會造成app的崩潰,對開發人員來說是透明的。
不需要修改現有的代碼,對現有代碼的侵入性降低到最低,不需要添加大量重復的邏輯判斷代碼。
全部源碼會在文章末尾提供,現截取部分代碼實現:
@implementation NSArray (NSRangeException) + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @autoreleasepool { [objc_getClass("__NSArray0") swizzleMethod:@selector(objectAtIndex:) swizzledSelector:@selector(emptyObjectIndex:)]; [objc_getClass("__NSArrayI") swizzleMethod:@selector(objectAtIndex:) swizzledSelector:@selector(arrObjectIndex:)]; [objc_getClass("__NSArrayM") swizzleMethod:@selector(objectAtIndex:) swizzledSelector:@selector(mutableObjectIndex:)]; [objc_getClass("__NSArrayM") swizzleMethod:@selector(insertObject:atIndex:) swizzledSelector:@selector(mutableInsertObject:atIndex:)]; } }); } - (id)emptyObjectIndex:(NSInteger)index{ return nil; } - (id)arrObjectIndex:(NSInteger)index{ if (index > = self.count || index < 0) { return nil; } return [self arrObjectIndex:index]; } - (id)mutableObjectIndex:(NSInteger)index{ if (index >= self.count || index < 0) { return nil; } return [self mutableObjectIndex:index]; } - (void)mutableInsertObject:(id)object atIndex:(NSUInteger)index{ if (object) { [self mutableInsertObject:object atIndex:index]; } } @end
越界的崩潰記錄有46條(總共崩潰記錄是304條),占15.13%,在crash界殺手排名第三。
殺手 NO.4:SIGPIPE 異常
先解釋一下什麼是SIGPIPE異常,通俗一點的描述是這樣的:對一個端已經關閉的socket調用兩次write,第二次write將會產生SIGPIPE信號,該信號默認結束進程。
那如何解決該問題呢?對SIGPIPE信號可以進行捕獲,也可將其忽略,對於iOS系統來說,只需要把下面這段代碼放在.pch文件中即可。
// 僅在 IOS 系統上支持 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
SIGPIPE的崩潰記錄有11條(總共304條崩潰記錄),占3.61%。在Crash界排名第四。
殺手 NO.5:SIGABRT 異常
這是一個讓程序終止的標識,會在斷言、app內部、操作系統用終止方法拋出。通常發生在異步執行系統方法的時候。如CoreData、NSUserDefaults等,還有一些其他的系統多線程操作。
注意:這並不一定意味著是系統代碼存在bug,代碼僅僅是成了無效狀態,或者異常狀態。
SIGABRT崩潰記錄9條(總共304條崩潰記錄),占2.96%。Crash界排名第五。
殺手總結
前面5大crash殺手,占了89.46%的崩潰率,解決了這5大crash殺手,基本上你的app就很健壯了,剩下的崩潰問題就需要具體問題具體分析了。
源碼下載地址
參考文章:
http://zhijianshusheng.github.io/2016/07/11/%E6%8C%89%E5%91%A8%E5%88%86%E7%B1%BB/20160711-0718/%E5%AF%BC%E8%87%B4iOS%E5%B4%A9%E6%BA%83%E7%9A%84%E6%9C%80%E5%B8%B8%E8%A7%815%E5%A4%A7%E5%85%83%E5%87%B6/
https://code.facebook.com/posts/583946315094347/automatic-memory-leak-detection-on-ios/
http://tech.glowing.com/cn/how-we-made-nsdictionary-nil-safe/
http://stackoverflow.com/questions/19560198/ios-app-error-cant-add-self-as-subview
https://my.oschina.net/moooofly/blog/474604
http://devma.cn/blog/2016/11/10/ios-beng-kui-crash-jie-xi/