這篇文章列出了9種常見的crash,原文寫得很好,我這裡對照我自己遇到過的情況再整理記錄下。
(一)KVO
KVO的一種常用場景是view物件監聽view model物件實現實時重新整理UI,例如有一個table view,每個cell都監聽對應的cell model,這樣資料來源陣列中只有一個物件的屬性發生改變時就不需要reload整個列表。
使用KVO有一個常見的crash就是沒有移除監聽,我們需要在dealloc方法中執行removeObserver方法。這裡推薦facebook開源的KVOController,讓我們更方便地使用KVO。
(二)遍歷可變集合時對集合做修改
我們經常會遇到集合遍歷的crash,有一點需要注意,在遍歷可變集合(NSMutableArray,NSMutableDictionary,NSMutableSet)時,不能夠對集合做修改,例如增加或刪除集合中的元素。這個問題最好是從程式碼規範上避免,例如介面中不應該暴露可變集合,而是暴露readonly的集合。以下是推薦的一種寫法:
People.h
#import <Foundation/Foundation.h>
@interface People : NSObject
@property (nonatomic, strong, readonly) NSArray *friends;
- (void)addFriend:(id)aFriend;
- (void)removeFriend:(id)aFriend;
@end複製程式碼
People.m
#import "People.h"
@interface People ()
@property (nonatomic, strong) NSMutableArray *internalFriends;
@end
@implementation People
- (void)dealloc
{
//
}
- (instancetype)init
{
self = [super init];
if (self) {
_internalFriends = [NSMutableArray new];
}
return self;
}
- (void)addFriend:(id)aFriend
{
if (aFriend == nil) {
return;
}
@synchronized(self)
{
[_internalFriends addObject:aFriend];
}
}
- (void)removeFriend:(id)aFriend
{
if (aFriend == nil) {
return;
}
@synchronized(self)
{
[_internalFriends removeObject:aFriend];
}
}
//NSMutableArray copy -> NSArray
- (NSArray *)friends
{
return [_internalFriends copy];
}
@end複製程式碼
還有一點要注意的是,對於第三方介面返回的集合,我們都要懷疑其正確性,有可能介面中寫明是不可變的但是實際返回的是可變集合,如果我們直接按照不可變來使用就有可能觸發crash,因此在集合遍歷前先對第三方介面返回的資料做一次copy操作是一個好的習慣。
(三)NSNotification
NSNotification是一種一對多的監聽機制,有一種常見的crash是物件dealloc後沒有移除監聽。
移除監聽的方式
我們可以根據具體的通知名稱移除,例如
[[NSNotificationCenter defaultCenter] removeObserver:self name:kSomeNotificationName object:someObject];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kSomeOtherNotificationName object:someOtherObject];
etc...複製程式碼
上述方法沒有問題,但是不利於維護,比如後期又有需求需要新增新的通知來實現,對應的就需要新增程式碼來移除,要是一不小心忘記移除就會觸發crash,更加推薦的方式是在dealloc中使用
[[NSNotificationCenter defaultCenter] removeObserver:self];
來移除
重複監聽
在註冊監聽通知時有一個問題需要注意,經測試,重複註冊會導致回撥方法進入多次,註冊幾次,回撥就會進入幾次。我們經常在viewDidLoad中註冊監聽,但是view是有可能unloaded再reloaded的,因此viewDidLoad就有可能執行多次導致重複註冊。
在init方法中註冊,在dealloc方法中移除
對於一個物件,它的init方法只會執行一次,dealloc方法也是,因此在這兩個方法中執行註冊和移除就能保證註冊和移除是平衡的,降低了問題排查的難度。
避免使用addObserverForName
[NSNotificationCenter addObserverForName:object:queue:usingBlock:]
提供了block的方法來使用通知,但是我們應該避免使用這種方式,因為這需要我們在後續程式碼裡單獨移除,這就增加了出錯的可能,不像上述提到的能在dealloc統一移除。
(四)處理空的情況
我們知道,在Objective-C中,對nil傳送訊息是沒有問題的,例如
[thing doStuff];
這種寫法沒有問題,但是如果引數是nil,則取決於具體的方法是如何實現的,例如:
[self doStuff:thing];
這種情況就要看thing是拿來做什麼,如果方法實現裡有如下程式碼
menuItem.title = thing;
menuItem是NSMenuItem,那麼當thing為空時就會導致crash。
一種推薦的做法是使用斷言對引數做空的判斷,具體如下:
- (void)someMethod:(id)someParameter {
NSParameterAssert(someParameter);
…do whatever…
}複製程式碼
(五)越界
常見的越界crash就是陣列越界,當然還有其他的越界,比如NSrange,對於這些的使用,推薦的做法是在使用前都做一下範圍校驗,這也是需要注意的點。
(六)非主執行緒處理UI事件
在非主執行緒處理UI事件會導致不可預知的事情發生,有可能crash,有可能是UI顯示異常。比如我們在子執行緒執行了一段耗時的計算任務,然後將計算結果傳遞給UI去更新顯示,這時候我們需要
dispatch_async(dispatch_get_main_queue(), ^{
});複製程式碼
另外,原文作者還提出了一些他的程式設計實踐經驗,例如:
- 應儘可能的將任務放到主執行緒排隊執行,這樣能避免大多數多執行緒問題,除非是經檢測有效能瓶頸的任務需要放到子執行緒,並且他也是偏向於將獨立的任務放到子執行緒中
- 儘可能使用點語法(_property = xxx的方式賦值不會觸發KVO)、ARC、weak屬性
- 建立完善的crash收集機制,並且將bug跟蹤記錄下來
- 程式碼寫出來應該是看起來很清晰的,如果看起來很繞,那麼是需要重構了
參考資料:inessential.com/hownottocra…
最後做個推廣,歡迎關注公眾號 MrPeakTech,我從這裡學到很多,推薦給大家,共同進步~