-[OC]-NSNotificationCenter-進階及自定義(附原始碼)
自
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
及其所捕獲的變數都不會釋放,從而導致記憶體洩露。
4、在必要時提前攔截通知的傳送
通知的使用在跨層和麵向多個物件通訊時十分便利,也因此而導致難以管理的問題頗受詬病,傳送通知時可能需要統一做一些工作,此時對通知進行攔截是必要的。NSNotificationCenter
是CFNotificationCenter
的封裝,有使用類似NSArray
的類簇設計,並採用了單例模式返回共享例項defaultCenter
。通過直接繼承的方式進行傳送通知的攔截是不可行的,因為獲得的是始終是靜態的單例物件,從Telegram
公司的ofollow,noindex">開源專案工程
中可以看到:通過借鑑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%2FReactiveCocoa" rel="nofollow,noindex">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
的使用以及通知的移除。
在不引入響應式框架的情況下,通過自定義通知名稱與觀察者的關係的方式,可以滿足要求。基本思路是:
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 同學說通知中心不安全,才嘗試自定義一個安全的通知中心。
原始碼
小結
通知中心,作為觀察者模式的運用,通過block
的運用可以有更靈活的表現,比如前文分享的Uber
用於解決通知中心難以管理的解決方案以 Uber-signals 一窺響應式
。
再到ReactiveCocoa
、RxSwift
函式響應式的思想的進一步抽象,程式設計的思維從命令式地呼叫一個方法/函式,轉換為因為某個通知/訊號而觸發了下一步的操作,值得去進一步探索。