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、在必要時提前攔截通知的傳送
通知的使用在跨層和麵向多個物件通訊時十分便利,也因此而導致難以管理的問題頗受詬病,傳送通知時可能需要統一做一些工作,此時對通知進行攔截是必要的。NSNotificationCenter
是 CFNotificationCenter
的封裝,有使用類似 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 同學說通知中心不安全,才嘗試自定義一個安全的通知中心。
原始碼
小結
通知中心,作為觀察者模式的運用,通過 block
的運用可以有更靈活的表現,比如前文分享的 Uber 用於解決通知中心難以管理的解決方案 以 Uber-signals 一窺響應式。
再到 ReactiveCocoa
、RxSwift
函式響應式的思想的進一步抽象,程式設計的思維從命令式地呼叫一個方法/函式,轉換為因為某個通知/訊號而觸發了下一步的操作,值得去進一步探索。
參考資料
Unregistering NSNotificationCenter Observers in iOS 9 Telegram 原始碼 Microsoft/WinObjc Reimplementate NSNotificationCenter Microsolf/WinObjc 真是一座金山啊