iOS開發的底線-崩潰

倦秋生發表於2019-03-04

前言

作為一個剛畢業從事iOS開發不久的人,最初同事以及導師都叮囑我寫程式碼的時候一定要注意異常情況,底線就是不能寫出任何有可能造成崩潰的程式碼。實際上,專案中有監測崩潰的工具,而且review的時候也會很嚴格檢查,所以基本上那種有可能造成崩潰的程式碼基本都會在上線前修正。

但就在前些天,客戶端發生了大面積一開啟就閃退的問題,影響非常嚴重,後來查出是引入的其他部門的SDK沒有進行型別判斷而導致的崩潰。或許是開發人員的不小心,但我覺得更多的是平時沒有養成習慣,沒有考慮到對於一個擁有千萬級別使用者的應用來說,即使是萬分之一的崩潰概率,也會有數千個使用者崩潰,這在競爭激烈的網際網路市場,是不能被容忍的。

我平時也沒有過度重視,因為我總覺得理論上應該不可能崩潰,但是實際的場景太多,理論上不可能並不是百分百不可能,作為足夠嚴謹的開發人員,必須守住自己的底線,不只是知道什麼情況會造成崩潰,而是要養成一種程式設計習慣,所以特意分析了各種崩潰的情況。

陣列越界

NSArray *firstNames = @[@"Roy", @"Mike", @"Jordan"];
NSString *name = firstNames[3];	// 崩潰

崩潰資訊:
**** Terminating app due to uncaught exception `NSRangeException`, reason: `*** -[__NSArrayI objectAtIndexedSubscript:]: index 3 beyond bounds [0 .. 2]` *****

分析:
可以看出當前陣列的範圍是0..2,當前下標超出了範圍,即訪問了未知的記憶體空間

注:
除了陣列可能越界之外,字串也有可能越界,例如執行substringWithRange:訊息時如果傳遞了過大的範圍也會崩潰
複製程式碼

字面量陣列和字典插入nil值

陣列

NSString *name;
NSArray *firstNames = @[@"Roy", @"Mike", @"Jordan", name];	//崩潰

崩潰資訊:
**** Terminating app due to uncaught exception `NSInvalidArgumentException`, reason: `*** -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[3]`*
*****

分析:
通過崩潰資訊可以很清楚看到是因為在字典初始化的時候插入了nil,實際上字面量語法是一種語法糖,本質是先建立了一個陣列,然後把方括號內的所有物件新增到這個陣列中

注:
字面量語法讓程式碼更加簡潔,也能及時發現錯誤,但是最後建立的陣列是不可變的
複製程式碼

字典

NSNumber *jordanAge;
NSDictionary *ages = @{@"Roy":@22, @"Mike":@24, @"Jordan":jordanAge};		//崩潰

崩潰資訊:
**** Terminating app due to uncaught exception `NSInvalidArgumentException`, reason: `*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[2]`*
*****

分析:
同上面原因一樣,都是插入了nil而導致的崩潰

注:
當key為nil的時候插入也會崩潰
複製程式碼

Unrecognized Selector

id person = @"person";
[person objectForKey:@"name"];	//崩潰

崩潰資訊:
**** Terminating app due to uncaught exception `NSInvalidArgumentException`, reason: `-[__NSCFConstantString objectForKey:]: unrecognized selector sent to instance 0x1000010e8`*
*****

分析:
person物件無法執行objectForKey:訊息,所以最後崩潰了

注:
在用Objective-C語言編碼時,我們會常常使用id型別更加便利地宣告變數,但在執行訊息前一定要確定它是否能響應,可使用respondsToSelector:檢查。最常見的場景是呼叫代理方法,即使指定了代理物件,也不一定保證代理實現了相應方法(協議裡還有可選實現的方法)
複製程式碼

NaN崩潰

float number = NAN;
NSDictionary *dict = @{@"value" : @(number)};
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingSortedKeys error:nil];

崩潰資訊:
**** Terminating app due to uncaught exception `NSInvalidArgumentException`, reason: `Invalid number value (NaN) in JSON write`*
*****

分析:
可以先來判斷dict物件是否能被轉換成JSON資料:
BOOL isValidJSONObject = [NSJSONSerialization isValidJSONObject:dict];
isValidJSONObject的結果是NO,也就是dict物件無法被轉換為JSON資料,即NaN型別不能被用於JSON物件中

注:
當進行不正常的數學運算時不只是會產生NaN型別,也有可能產生+inf型別,雖然並不會直接造成崩潰,但有可能在用它們進行其他操作的時候會有可能造成崩潰。通過isnan(x)和isinf(x)方法可以判斷nan和inf型別
複製程式碼

富文字初始化時字串為空

NSString *text;
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text]; // 崩潰

崩潰資訊:
**** Terminating app due to uncaught exception `NSInvalidArgumentException`, reason: `NSConcreteAttributedString initWithString:: nil value`*
*****

分析:
從崩潰資訊中可以很明顯看到是因為傳入的變數值為nil而崩潰

注:
構造NSMutableString時,如果傳入的字串為nil也會崩潰
複製程式碼

建立未註冊過的UITableViewCell

UITableViewCell *cell = [tableView    
dequeueReusableCellWithIdentifier:@"reuseIdentifier" 
forIndexPath:indexPath]	// 崩潰
複製程式碼

伺服器端出現問題而造成的崩潰

有一種情況,就是伺服器端傳遞資料給客戶端,客戶端將其解析成模型物件,然後取模型裡的值插入字面量語法構造的陣列或者字典中。如果伺服器端發生了問題,而客戶端沒有保護措施就會受到連累,當然實際上伺服器端百分之九十九的概率是不可能發生問題的,所以很多人(包括我)也理所當然不會去特意多寫一些多餘的防禦性程式碼。
我上面只是舉了一個例子,一般我們都會和伺服器端約定好資料格式以及其他細節,而且大多數時候都會做一些保護,但我真正想強調的是客戶端不崩潰一定優於客戶端依賴於伺服器端而不崩潰,儘可能避免受到外界的影響

其他崩潰

  1. 在iOS 9.0之後NSNotificationCenter不會對一個dealloc的觀察者傳送訊息,所以如果應用最低版本是9.0,其實也不必每次都去移除通知,但如果需要支援更低的版本,還是一定要移除通知,否則會崩潰
  2. KVO不移除監聽會導致奔潰,所以KVO的新增和移除必須成雙成對
  3. 記憶體洩露,最著名的是代理和被代理物件迴圈引用

總結

以上都是我曾經遇到過的崩潰情況,當然還有很多我不知道的情況,畢竟技術是複雜的。我們或許可以使用一些工具來檢查或者避免崩潰,但我還是想強調平時對待程式碼要更加嚴謹,對待負責的專案要更有責任感

上述程式碼的CrashDemos連結:github.com/iroyzhang/i…

相關文章