自 iOS 9
開始(見 release notes ),Foundation
調整了 NSNotificationCenter
對觀察者的引用方式( zeroing weak reference
),再也不給已釋放的觀察者發送通知,所以以往在 dealloc
時移除觀察者的作法能夠省去。html
若是是須要適配 iOS 8
,那麼 UIViewController
及其子類能夠省去移除通知的過程(親測有效),而其餘對象則須要在 dealloc
前移除觀察者。ios
感謝 Ace 同窗第一時間的測試發現git
控制器對象對於通知的監聽一般是在生命週期的 viewDidLoad
方法處理,也就是說,在 viewDidLoad
以前,還未添加觀察者,對應地在在移除通知通知時能夠作是否加載了視圖的判斷以下:github
- (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;
}
複製代碼
NSNotificationCenter
是支持 block
手法的自 iOS 4
開始通知中心即支持 block
回調,其 API
以下:app
- (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 形式的通知中心觀察者是否須要手動註銷通知的使用在跨層和麪向多個對象通訊時十分便利,也所以而致使難以管理的問題頗受詬病,發送通知時可能須要統一作一些工做,此時對通知進行攔截是必要的。NSNotificationCenter
是 CFNotificationCenter
的封裝,有使用相似 NSArray
的類簇設計,並採用了單例模式返回共享實例 defaultCenter
。經過直接繼承的方式進行發送通知的攔截是不可行的,由於得到的是始終是靜態的單例對象,從 Telegram
公司的開源項目工程中能夠看到:經過借鑑 KVO
的實現原理,將單例對象的類修改成特定的子類,從而實現通知的攔截。post
第一步,修改通知中心單例的類:
@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:攔截以後能夠發現系統發送通知的數量和頻率真高,從這個側面看發送通知的性能問題不用太過顧忌。
既不肯意手動移動通知,又想使用 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
的使用以及通知的移除。
在不引入響應式框架的狀況下,經過自定義通知名稱與觀察者的關係的方式,能夠知足要求。基本思路是:
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 真是一座金山啊