[iOS] [OC] NSNotificationCenter 進階及自定義(附原始碼)

席萍萍Brook發表於2018-09-11

1、並不總是需要移除觀察者

iOS 9 開始(見 release notes ),Foundation 調整了 NSNotificationCenter 對觀察者的引用方式( zeroing weak reference),不再給已釋放的觀察者傳送通知,因此以往在 dealloc 時移除觀察者的做法可以省去。

如果是需要適配 iOS 8,那麼 UIViewController及其子類可以省去移除通知的過程(親測有效),而其他物件則需要在 dealloc 前移除觀察者。

感謝 Ace 同學第一時間的測試發現

2、控制器新增和移除觀察者的良好實踐

控制器物件對於通知的監聽通常是在生命週期的 viewDidLoad 方法處理,也就是說,在 viewDidLoad 之前,還未新增觀察者,對應地在在移除通知通知時可以做是否載入了檢視的判斷如下:

- (void)dealloc {
    if (self.isViewLoaded) {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
}
複製程式碼

這一點 isViewLoaded 的判斷,對於 NSNotification 的監聽來說不是必要的,因為在未監聽通知的情況下,呼叫 removeObserver: 方法是仍舊是安全的,而 KVO ( key-value observing,則不然。因為 KVO 在未監聽的情況下移除觀察者是不安全的,所以如果是在 viewDidLoad 監聽KVO ,則 KVO 的移除就需要執行判斷:

- (void)dealloc {
    if (self.isViewLoaded) {
        [self removeObserver:someObj forKeyPath:@"someKeyPath"];
    }
}

複製程式碼

此外,很多時候控制器的檢視還未載入,也需要監聽特定的通知,此時通知的監聽適合在構造方法 initWithNibName:bundle 方法中監聽,此構造方法在程式碼或者 Interface Builder 構建例項時都會呼叫:

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(onNotification:)
                                                     name:@"kNotificationName"
                                                   object:nil];
    }
    
    return self;
}
複製程式碼

3、系統 NSNotificationCenter 是支援 block 手法的

iOS 4 開始通知中心即支援 block 回撥,其 API 如下:

- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name
                             object:(nullable id)obj
                              queue:(nullable NSOperationQueue *)queue
                         usingBlock:(void (^)(NSNotification *note))block
                                    NS_AVAILABLE(10_6, 4_0);
複製程式碼

回撥可以指定操作佇列,並返回一個觀察者物件。呼叫示例:

- (void)observeUsingBlock {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    observee = [center addObserverForName:@"kNotificationName"
                                   object:nil
                                    queue:[NSOperationQueue mainQueue]
                               usingBlock:^(NSNotification * _Nonnull note) {
                                   NSLog(@"got the note %@", note);
                               }];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:observee];
}

複製程式碼

其中,有幾點值得注意

  • 方法返回一個 id<NSObject> 監聽者物件,其實是系統的私有類的例項,因為沒必要暴露其具體型別和介面,所以用一個 id<NSObject> 物件指明用途,從中可見協議的又一個應用場景。
  • 這個返回值物件是充當了原來的 target-action 的封裝實現,在其內部觸發了 action 後呼叫起初傳入的 block 引數。
  • 返回的觀察者和 block 都會被通知中心所持有,因此使用者有義務在必要的時候呼叫 removeObserver: 方法,將此監聽移除,否則監聽者和 block及其所捕獲的變數都不會釋放,從而導致記憶體洩露。此處詳細的說明和解決方案可以參考 SwiftGG翻譯組的翻譯文章 Block 形式的通知中心觀察者是否需要手動登出

4、在必要時提前攔截通知的傳送

通知的使用在跨層和麵向多個物件通訊時十分便利,也因此而導致難以管理的問題頗受詬病,傳送通知時可能需要統一做一些工作,此時對通知進行攔截是必要的。NSNotificationCenterCFNotificationCenter 的封裝,有使用類似 NSArray 的類簇設計,並採用了單例模式返回共享例項 defaultCenter。通過直接繼承的方式進行傳送通知的攔截是不可行的,因為獲得的是始終是靜態的單例物件,從 Telegram 公司的開源專案工程中可以看到:通過借鑑 KVO 的實現原理,將單例物件的類修改為特定的子類,從而實現通知的攔截。

第一步,修改通知中心單例的類:

@interface GSNoteCenter : NSNotificationCenter

@end


/// 修改單例的類為一個子類的型別
void hack() {
    id center = [NSNotificationCenter defaultCenter];
    object_setClass(center, GSNoteCenter.class);
}
複製程式碼

第二步,攔截通知的傳送事件: 利用繼承多型特性,在傳送通知的前後進行攔截:

@implementation GSNoteCenter

- (void)postNotificationName:(NSNotificationName)aName
                      object:(id)anObject
                    userInfo:(NSDictionary *)aUserInfo
{
    // do something before post
    [super postNotificationName:aName
                         object:anObject
                       userInfo:aUserInfo];
    // do something after post
}

@end

複製程式碼

PS:攔截之後可以發現系統傳送通知的數量和頻率真高,從這個側面看傳送通知的效能問題不用太過顧忌。

5、自定義不需要移除監聽的 block 的通知中心(附原始碼)

既不願意手動移動通知,又想使用 block 實現通知監聽,那麼必要的封裝是必須的。比如, ReactiveCocoa 中的實現如下:

@implementation NSNotificationCenter (RACSupport)

- (RACSignal *)rac_addObserverForName:(NSString *)notificationName object:(id)object {
	@unsafeify(object);
	return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
		@strongify(object);
		id observer = [self addObserverForName:notificationName
                                        object:object
                                         queue:nil
                                    usingBlock:^(NSNotification *note) {
			[subscriber sendNext:note];
		}];

		return [RACDisposable disposableWithBlock:^{
			[self removeObserver:observer];
		}];
	}] setNameWithFormat:@""];
}
@end
複製程式碼

將通知作為一個訊號源,直接訂閱 next 收聽結果即可,十分優雅地解決了 block 的使用以及通知的移除。

在不引入響應式框架的情況下,通過自定義通知名稱與觀察者的關係的方式,可以滿足要求。基本思路是:

  • 以一個觀察者對應一個 block,存入一個類似字典的集合,但需要實現當觀察者釋放時同時也釋放 block,這裡考慮採用支援弱引用的集合,比如 NSMapTable
  • 觀察者對通知名進行監聽,因此一個通知名對應了一個集合,當觸發一個通知名時,通知集合內的存在的所有觀察者。
  • 通知中心統一持有所有通知名及其關聯關係。

由此實現的初步封裝完成放在 GitHub,通知的註冊如下:

- (void)registerBlock:(GSNoticeBlock)block service:(NSString *)service forObserver:(id)observer {
    GSServiceMap *mapModel = [self mapForService:service];
    [mapModel.map setObject:block forKey:observer];
}

複製程式碼

通知的觸發如下:

- (void)triggerService:(NSString *)service userInfo:(id)userInfo {
    GSServiceMap *mapModel = [self mapForService:service];
    NSString *key = nil;
    NSEnumerator *enumerator = [mapModel.map keyEnumerator];
    while (key = [enumerator nextObject]) {
        GSNoticeBlock block = [mapModel.map objectForKey:key];
        !block ?: block(userInfo);
    }
}
複製程式碼

如果需要提前移除監聽,操作如下:

- (void)unregisterService:(NSString *)service forObserver:(id)observer {
    GSServiceMap *mapModel = [self mapForService:service];
    [mapModel.map removeObjectForKey:observer];
}
複製程式碼

感謝 Mark 同學說通知中心不安全,才嘗試自定義一個安全的通知中心。

原始碼

GitHub

小結

通知中心,作為觀察者模式的運用,通過 block 的運用可以有更靈活的表現,比如前文分享的 Uber 用於解決通知中心難以管理的解決方案 以 Uber-signals 一窺響應式

再到 ReactiveCocoaRxSwift 函式響應式的思想的進一步抽象,程式設計的思維從命令式地呼叫一個方法/函式,轉換為因為某個通知/訊號而觸發了下一步的操作,值得去進一步探索。

參考資料

Unregistering NSNotificationCenter Observers in iOS 9 Telegram 原始碼 Microsoft/WinObjc Reimplementate NSNotificationCenter Microsolf/WinObjc 真是一座金山啊

相關文章