[iOS] [OC] NSNotificationCenter 進階及自定義(附源代碼)

一、並不老是須要移除觀察者

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 形式的通知中心觀察者是否須要手動註銷

四、在必要時提早攔截通知的發送

通知的使用在跨層和麪向多個對象通訊時十分便利,也所以而致使難以管理的問題頗受詬病,發送通知時可能須要統一作一些工做,此時對通知進行攔截是必要的。NSNotificationCenterCFNotificationCenter 的封裝,有使用相似 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 的通知中心(附源代碼)

既不肯意手動移動通知,又想使用 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 真是一座金山啊

相關文章
相關標籤/搜索