透徹理解 NSNotificationCenter 通知(附實現程式碼)
推薦另一篇文章:透徹理解 KVO 觀察者模式(附基於runtime實現程式碼)
寫在前面
NSNotificationCenter
這個東西作為iOS工程師想必都不陌生,但是有人可能連引數的意義都沒搞明白,寫這篇文章的目的不止是為了讓不會用的人會用,更是為了讓會用的人理解得更透徹。本篇文章主要是梳理NSNotificationCenter
的特性和值得注意的地方,並且在後面結合對其特性的分析手動利用程式碼來實現它。
一、分析
1、 基本使用方法
直接進入NSNotification
檔案。
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
該屬性是獲取NSNotificationCenter
唯一單例,它就是一個訊息分發中心,通過使用這個唯一的例項我們進行新增通知、傳送通知、移除通知
。
(1) 新增通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondsToNotification:) name:@"test0" object:_obj0];
_obj0
是建立的一個例項,這裡暫時不討論object
引數的用法。Observer
即為響應者無需多說;selector
即為一個響應通知的方法,需要SEL
型別;name
是一個標識,通知中心主要是通過它來實現訊息的精確分發(當然object也有定位作用)。
(2) 傳送通知
//便利方法[[NSNotificationCenter defaultCenter] postNotificationName:@"test0" object:_obj0 userInfo:@{@"key":@"_obj0"}];//使用NSNotificationNSNotification *notification = [[NSNotification alloc] initWithName:@"test0" object:_obj2 userInfo:@{@"key":@"_obj2"}]; [[NSNotificationCenter defaultCenter] postNotification:notification];
傳送通知和新增通知對應,需要name、object
引數,這裡多了一個userInfo
,該引數可以把你需要攜帶的資料傳送給該通知的響應者。
其實我們可以很輕易的想到,便利傳送通知方法不過是對於使用NSNotification
傳送通知的一個語法糖,NSNotification
才是訊息體。
(3) 移除通知
//移除該響應者的全部通知[[NSNotificationCenter defaultCenter]removeObserver:self];//移除該響應者 name==@"test0" 的全部通知[[NSNotificationCenter defaultCenter] removeObserver:self name:@"test0" object:nil];//移除該響應者 name==@"test0" 且 object==_obj0 的全部通知[[NSNotificationCenter defaultCenter] removeObserver:self name:@"test0" object:_obj0];
移除通知這裡有點講究,從上至下越來越“精準” 。
在合理的位置移除通知是至關重要的:
1、讓不希望繼續接受通知的響應者失去對該通知的響應;
2、避免重複新增相同通知(響應者的記憶體為同一塊的時候);
3、通知中心對響應者observer
是使用unsafe_unretained
修飾,當響應者釋放會出現野指標,向野指標傳送訊息造成崩潰;在iOS 9(更新的系統版本有待考證)之後,蘋果對其做了優化,會在響應者呼叫dealloc
方法的時候執行removeObserver:
方法。
注意:在後文會詳細分析該問題。
當然,常規的業務場景一般是在該響應者釋放的時候移除。
- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; }
(4) 響應通知
- (void)respondsToNotification:(NSNotification *)noti {id obj = noti.object;NSDictionary *dic = noti.userInfo;NSLog(@"\n- self:%@ \n- obj:%@ \n- notificationInfo:%@", self, obj, dic); }
響應通知的時候會將NSNotification
訊息體傳遞過來,如程式碼所示。
2、object:(nullable id)anObject引數
-
新增通知時,若指定了
object
引數,那麼該響應者只會接收傳送通知 時object
引數指定為同一例項的通知。 -
傳送通知時,若指定了
object
引數,並不會影響新增通知 時沒有指定object
引數的響應者接收通知。
如果感覺有點繞,看如下程式碼便知。
//新增通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondsToNotification:) name:@"test0" object:nil];//傳送通知 [[NSNotificationCenter defaultCenter] postNotificationName:@"test0" object:_obj0];//由於新增通知時,object==nil,所以該響應者仍然能接收到該通知。
//新增通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondsToNotification:) name:@"test0" object: _obj0];//傳送通知 [[NSNotificationCenter defaultCenter] postNotificationName:@"test0" object:nil];//由於新增通知時,指定了object==_obj0,而傳送通知時,object==nil,所以無法接收到通知//(只有當object==_obj0才能接收到通知)。
3、通知執行緒問題
我們進入全域性佇列傳送這個通知
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{NSLog(@"傳送通知 currentThread : %@", [NSThread currentThread]); [[NSNotificationCenter defaultCenter] postNotificationName:@"test0" object:nil]; });
在接收通知的地方將執行緒打印出來
傳送通知 currentThread : {number = 3, name = (null)} 響應通知 currentThread : {number = 3, name = (null)}
結論:通知傳送執行緒和通知接收執行緒是一致的。
由此看來,如果當我們不是百分之百確認通知的傳送佇列是在主佇列中時,我們最好加上如下程式碼從而對我們的UI進行處理。
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {//UI處理} else { dispatch_async(dispatch_get_main_queue(), ^{//UI處理 }); }
4、是否需要移除通知?
以下程式碼模擬重複新增通知的情況,所以如果可能會重複新增通知,我們都應該做好相應的處理。
for (int i = 0; i < 3; i++) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondsToNotification:) name:@"test0" object:nil]; }//該程式碼導致的結果是,響應通知回撥會走三次。
可能有人會問,為什麼系統庫沒有做個重複新增的判斷?當然,這可能是為了讓我們更靈活的運用,也可能是對時間複雜度的一種妥協吧
有過比較長開發經驗的同學應該都有過,沒有及時的移除通知而導致意外崩潰的情況。前面也說過,通知中心對響應者observer
是使用unsafe_unretained
修飾,當響應者釋放會出現野指標,如果向野指標傳送訊息會造成崩潰。在 iOS9 系統之後,[NSNotificationCenter defaultCenter]
會在響應者observer
呼叫-dealloc
方法的時候執行-removeObserver:
方法。
在官方文件中有這樣一段話:
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its
dealloc
method.
動手做個小實驗:
新建一個NSNotificationCenter
的分類,程式碼如下:
@implementation NSNotificationCenter (YB)+ (void)load { Method origin = class_getInstanceMethod([self class], @selector(removeObserver:)); Method current = class_getInstanceMethod([self class], @selector(_removeObserver:)); method_exchangeImplementations(origin, current); } - (void)_removeObserver:(id)observer {NSLog(@"呼叫移除通知方法: %@", observer);//[self _removeObserver:observer];}@end
然後新建一個類正常的使用通知,但是請不要手動在-dealloc
中釋放通知(我們要做實驗)。然後我們釋放掉這個類(可以使用控制器present、dismiss)。
呼叫移除通知方法: <test_vc: nbsp="" 0x7f9a0a4d9240=""></test_vc:>
神奇的現象發生了,通過比較記憶體地址,[NSNotificationCenter defaultCenter]
確實是呼叫了removeObserver :
方法移除對應響應者的通知監聽。
注意上面的程式碼中,我將[self _removeObserver:observer];
註釋掉了,意味著該方法已經被我截取了,我們再向該“移除通知未遂”的響應者observer
傳送通知,直接崩潰。當去除註釋,正常執行,無需手動移除。
結論:如果iOS支援版本在 iOS9 以上,多數情況理論上可以不用移除通知,但是由於歷史遺留、開發者習慣等因素,看個人喜好了
二、程式碼實現
心血來潮,看著NSNotification.h
的API和本著對其的理解,決定著手實現一波。
其實仔細一想,通知的功能類似於一個路由,它的基本實現思路並不複雜。我們要做的無非是“新增”、“傳送”、“移除”三件事。
但是在具體實現中,還是有些比較麻煩的地方,下面具體敘述(最好下載demo便於理解)。
1、新增通知
首先,建立了一個YBNotificationCenter
類,屬性如下:
@property (class, strong) YBNotificationCenter *defaultCenter;@property (strong) NSMutableDictionary *observersDic;
defaultCenter
類屬性不用說,它是唯一單例(具體實現看程式碼);observersDic
即為用來儲存新增通知相關資訊的字典。
然後建立了一個YBObserverInfoModel
類,屬性如下:
@property (weak) id observer;@property (strong) id observer_strong;@property (strong) NSString *observerId;@property (assign) SEL selector;@property (weak) id object;@property (copy) NSString *name;@property (strong) NSOperationQueue *queue;@property (copy) void(^block)(YBNotification *noti);
該類就是響應者資訊儲存模型類,也就是會放在上面observersDic
字典內的元素。先回憶一下,當我們使用- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;
或- (id
方法時,這些配置的變數是不是在YBObserverInfoModel
都有體現呢?
是的,新增通知的操作不過就是將我們需要配置的變數統統儲存起來,但是注意幾點:一是對observer
和object
不能強持有,否則其無法正常釋放;二是對name
屬性最好使用copy
修飾,保證其不會受外部干擾;三是observer_strong
屬性是在使用程式碼塊回撥的那個新增通知方法時,需要使用到的強引用屬性;四是observerId
屬性會比較陌生,它的作用大家可以先不管,之後會有用處。
新增通知核心程式碼
- (void)addObserverInfo:(YBObserverInfoModel *)observerInfo {//新增進observersDic NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic;@synchronized(observersDic) {NSString *key = (observerInfo.name && [observerInfo.name isKindOfClass:NSString.class]) ? observerInfo.name : key_observersDic_noContent;if ([observersDic objectForKey:key]) {NSMutableArray *tempArr = [observersDic objectForKey:key]; [tempArr addObject:observerInfo]; } else {NSMutableArray *tempArr = [NSMutableArray array]; [tempArr addObject:observerInfo]; [observersDic setObject:tempArr forKey:key]; } } }
我們傳入一個配置好的YBObserverInfoModel
模型進入方法,構建一個樹形結構,用傳入的name
作為key
(如果name
為空使用key_observersDic_noContent
常量代替),把所有使用相同name
的通知放進同一個陣列作為value
,並且添加了執行緒鎖保證observersDic
資料讀寫安全。
這麼做的理由:在通知的整個功能體系中,“新增”、“傳送”、“移除”哪一步對效率的要求最高?毫無疑問是“傳送”的時候,我們通常使用- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject
方法傳送通知,aName
引數將是我們找到對應通知的第一匹配點。如果我們將其它引數作為observersDic
的key
,我們傳送通知的時候不得不遍歷整個observersDic
;而如上程式碼實現,傳送通知的時候,直接就能通過key
直接找到對應的通知資訊了,有效降低了時間複雜度。
使用程式碼塊回撥通知方法的實現
- (id <nsobject> )addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(YBNotification * _Nonnull))block {if (!block) {return nil; } YBObserverInfoModel *observerInfo = [YBObserverInfoModel new]; observerInfo.object = obj; observerInfo.name = name; observerInfo.queue = queue; observerInfo.block = block;NSObject *observer = [NSObject new]; observerInfo.observer_strong = observer; observerInfo.observerId = [NSString stringWithFormat:@"%@", observer]; [self addObserverInfo:observerInfo];return observer; } </nsobject>
這裡有個地方需要提出來談談,在使用系統的這個方法的時候,一經實驗就能發現,不管我們強引用或者弱引用這個返回值id
時,都能在業務類dealloc釋放的時候有效的移除該通知。
由於使用該方法新增通知的時候不會傳入observer
引數,這裡建立了一個observer
,如果這裡使用observerInfo.observer = observer;
,而業務類沒有強引用這個返回值observer
,它將會自然釋放。所以,這裡做了一個特殊處理,讓observerInfo
例項強持有observer
。
值得注意的是,外部如果強引用返回的id
型別的observer
,會造成observer
無法及時的釋放,但是這點記憶體我認為還是可以接受的,當然業務類使用弱引用該observer
是最好的選擇。
2、傳送通知
和系統通知一樣,同樣建立了一個類YBNotification
傳送通知訊息體,屬性就我們熟悉的幾個:
@property (copy) NSString *name;@property (weak) id object;@property (copy) NSDictionary *userInfo;
然後將
兩個協議實現一下就好了,具體看demo。
傳送通知核心程式碼
- (void)postNotification:(YBNotification *)notification {if (!notification) {return; }NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic;NSMutableArray *tempArr = [observersDic objectForKey:notification.name];if (tempArr) { [tempArr enumerateObjectsUsingBlock:^(YBObserverInfoModel *obj, NSUInteger idx, BOOL * _Nonnull stop) {if (obj.block) {if (obj.queue) {NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ obj.block(notification); }];NSOperationQueue *queue = obj.queue; [queue addOperation:operation]; } else { obj.block(notification); } } else {if (!obj.object || obj.object == notification.object) {#pragma clang diagnostic push#pragma clang diagnostic ignored "-Warc-performSelector-leaks" obj.observer?[obj.observer performSelector:obj.selector withObject:notification]:nil;#pragma clang diagnostic pop } } }]; } }
傳送通知相對簡單,只需要分清是使用程式碼塊回撥,還是通過執行SEL回撥。在使用程式碼塊回撥時,如果傳入了佇列queue
,就讓該程式碼塊在該佇列中執行,否則正常執行。
!obj.object || obj.object == notification.object
if語句中這個判斷值得注意。
3、移除通知
移除通知本身簡單,有些麻煩的是自動移除。先貼上移除程式碼:
- (void)removeObserverId:(NSString *)observerId name:(NSString *)aName object:(id)anObject {if (!observerId) {return; }NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic;@synchronized(observersDic) {if (aName && [aName isKindOfClass:[NSString class]]) {NSMutableArray *tempArr = [observersDic objectForKey:[aName mutableCopy]]; [self array_removeObserverId:observerId object:anObject array:tempArr]; } else { [observersDic enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSMutableArray *obj, BOOL * _Nonnull stop) { [self array_removeObserverId:observerId object:anObject array:obj]; }]; } } } - (void)array_removeObserverId:(NSString *)observerId object:(id)anObject array:(NSMutableArray *)array {@autoreleasepool { [array.copy enumerateObjectsUsingBlock:^(YBObserverInfoModel *obj, NSUInteger idx, BOOL * _Nonnull stop) {if ([obj.observerId isEqualToString:observerId] && (!anObject || anObject == obj.object)) { [array removeObject:obj];return; } }]; } }
所有移除通知的方法,最終落腳點都是在這裡。
上面方法中,如果aName不是合理的,就需要遍歷observersDic
移除對應的通知;如果aName是合理的,就直接查詢對應的陣列移除內容。
使用observerId
屬性移除通知,而不用observer
響應者來直接比較移除:
還記得新增通知時YBObserverInfoModel
類的@property (strong) NSString *observerId;
屬性麼?在新增通知的時候,我將響應者的地址資訊作為該屬性的值(保證其唯一性):
observerInfo.observerId = [NSString stringWithFormat:@"%@", observer];
然後在移除的時候通過比較進行相應的操作。
實現自動移除通知(解釋為何使用observerId移除通知而不用observer)
實現自動移除通知,思路是在響應者observer
走dealloc
的時候移除對應的通知,難點就是在ARC中是不允許對dealloc
做繼承和交換方法等操作的,所以我使用了一個緩兵之計——動態給observer
新增一個屬性,我們監聽這個屬性的dealloc
方法移除對應的通知,程式碼如下:
- (void)addObserverInfo:(YBObserverInfoModel *)observerInfo { //為observer關聯一個釋放監聽器 id resultObserver = observerInfo.observer?observerInfo.observer:observerInfo.observer_strong;if (!resultObserver) {return; } YBObserverMonitor *monitor = [YBObserverMonitor new]; monitor.observerId = observerInfo.observerId;const char *keyOfmonitor = [[NSString stringWithFormat:@"%@", monitor] UTF8String]; objc_setAssociatedObject(resultObserver, keyOfmonitor, monitor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //新增進observersDic NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic;@synchronized(observersDic) {NSString *key = (observerInfo.name && [observerInfo.name isKindOfClass:NSString.class]) ? observerInfo.name : key_observersDic_noContent;if ([observersDic objectForKey:key]) {NSMutableArray *tempArr = [observersDic objectForKey:key]; [tempArr addObject:observerInfo]; } else {NSMutableArray *tempArr = [NSMutableArray array]; [tempArr addObject:observerInfo]; [observersDic setObject:tempArr forKey:key]; } } }
只不過在新增通知到observersDic
之前,新增一個monitor
例項,使用objc_setAssociatedObject
動態關聯方法給resultObserver
新增一個強引用的屬性,注意objc_setAssociatedObject
方法的第二個引數必須保證其唯一性,因為同一個響應者可能新增多個通知。
好了,現在基本工作都完成了,只需要在這個YBObserverMonitor
方法中做簡單的移除邏輯就OK了,程式碼如下:
//監聽響應者釋放類@interface YBObserverMonitor : NSObject@property (strong) NSString *observerId;@end@implementation YBObserverMonitor- (void)dealloc {NSLog(@"%@ dealloc", self); [YBNotificationCenter.defaultCenter removeObserverId:self.observerId]; }@end
變數的釋放順序各種不確定,可能走YBObserverMonitor
的dealloc
時,observer
響應者物件已經釋放了,所以不直接使用observer
響應者物件對比做釋放操作。
寫在後面
關於實現部分,雖然我做了個大致的測試,可能還是會存在一些潛在的問題,希望各位大佬不惜筆墨點撥一番
附:NSNotification 程式碼實現Demo地址
作者:indulge_in
連結:https://www.jianshu.com/p/e3a38b21420c