iOS開發之避免crash

hi_xgb發表於2016-12-10

這篇文章列出了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,我從這裡學到很多,推薦給大家,共同進步~

iOS開發之避免crash

相關文章