通知概念
蘋果官方文檔有一段對通知的介紹如下:
A notification is a message sent to one or more observing objects to inform them of an event in a program. The notification mechanism of Cocoa follows a broadcast model.
通知機制的核心是一個與線程關聯的單例對象叫通知中心(NSNotificationCenter)。通知中心發送通知給觀察者是同步的,也可以用通知隊列(NSNotificationQueue)異步發送通知。
NSNotification
NSNotification
包含了如下必要字段且均是只讀的:
@property (readonly, copy) NSNotificationName name; // 通知名稱,通知的唯一標識
@property (nullable, readonly, retain) id object; // 任意對象,通常是通知發送者
@property (nullable, readonly, copy) NSDictionary *userInfo; // 通知的附加信息
可以通過Designaged Initializer函數創建NSNotification
的實例對象:
// 指定初始化函數
- (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo NS_DESIGNATED_INITIALIZER;
也可以通過NSNotification (NSNotificationCreation)
分類相應的方法創建NSNotification
的實例對象。
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
- (instancetype)init /*NS_UNAVAILABLE*/; /* do not invoke; not a valid initializer for this class */
NSNotification
對象是不可變的。
NSNotification
不能通過init
初始化,否則會引起如下崩潰:
但是一般情況下不會直接這樣創建通知對象。實際開發中更多的是直接調用NSNoficationCenter
的postNotificationName:object:
或postNotificationName:object:userInfo:
方法發送通知,這兩個方法內部會根據傳入的參數直接創建通知對象。
NSNotificationCenter
NSNotificationCenter
提供了一種互不相干的對象之間能夠相互通信的方式。它接收NSNotification
對象并把通知廣播給所有感興趣的對象。
NSNotificationCenter
暴露給外部的屬性只有一個defaultCenter
,而且這個屬性還是只讀的。
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
暴露給外部的方法分為三類:添加通知觀察者的方法、發出通知的方法、移除通知觀察者的方法。
// 添加通知觀察者
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;
// 發出通知
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
// 移除通知觀察者
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
注意:
若notificationName為
nil
,通知中心會通知所有與該通知中object相匹配的監聽對象。若anObject為
nil
,通知中心會通知所有與該通知中notificationName相匹配的監聽對象。-
iOS9以后
NSNofitifcationCenter
無需手動移除觀察者。在觀察者對象釋放之前,需要調用==removeOberver==方法將觀察者從通知中心移除,否則程序可能會出現崩潰。但從iOS9開始,即使不移除觀察者對象,程序也不會出現異常。
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.這是因為在iOS9以后,通知中心持有的觀察者由==unsafe_unretained==引用變為==weak==引用。即使不對觀察者手動移除,持有的觀察者的引用也會在觀察者被回收后自動置空。但是通過
addObserverForName:object: queue:usingBlock:
方法注冊的觀察者需要手動釋放,因為通知中心持有的是它們的強引用。
NSNotificationQueue
NSNotificationQueue
通知隊列充當通知中心的緩沖區。通知隊列通常以FIFO
(先進先出)的順序來維護通知。每個線程都有一個與缺省通知中心(default notification center)相關的缺省通知隊列(defaultQueue)。
// 缺省的通知隊列
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;
// 指定初始化函數
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
通過defaultQueue
獲取默認的通知隊列或者通過指定初始化函數initWithNotificationCenter:
創建通知隊列,最終都是通過NSNotificationCenter
來發送、注冊通知。
// 往通知隊列添加通知
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes; // 如果modes為nil,則對于runloop的所有模式發送通知都是有效的
// 移除通知隊列中的通知
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
合并通知
NSNotificationNoCoalescing
. 不合并隊列中的通知NSNotificationCoalescingOnName
. 按通知名稱合并通知NSNotificationCoalescingOnSender
. 按傳入的object合并通知
發送方式
NSPostASAP
. 在當前通知調用結束或計時器超時發送通知NSPostWhenIdle
. 當runloop處于空閑狀態時發送通知NSPostNow
. 在合并通知后立即發送通知
NSNotification在多線程中的使用
無論在哪個線程中注冊了觀察者,通知的發送和接收都是在同一個線程中。所以當接收到通知做UI操作的時候就需要考慮線程的問題。如果在子線程中接收到通知,需要切換到主線程再做更新UI的操作。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
static NSString * const NOTIFICATION_NAME = @"notification_name";
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:NOTIFICATION_NAME object:nil];
NSLog(@"Register Observer. Current thread = %@", [NSThread currentThread]);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Post Notification. Current thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:nil];
});
}
- (void)handleNotification:(NSNotification *)n {
NSLog(@"Received Notification. Current thread = %@", [NSThread currentThread]);
}
運行接口如下:
Register Observer. Current thread = <NSThread: 0x6000015fd3c0>{number = 1, name = main}
Post Notification. Current thread = <NSThread: 0x60000157cf00>{number = 5, name = (null)}
Received Notification. Current thread = <NSThread: 0x60000157cf00>{number = 5, name = (null)}
在主線程注冊了觀察者,然后在子線程發送通知,最后接收和處理通知也是在子線程。一般情況下,發送通知所在的線程就是接收通知所在的線程。
將通知重定向到指定線程
解決方法:捕獲發送通知所在線程的通知,然后將其重定向至指定線程。關于通知重定向,官方文檔給出了一種解決方法。
一種重定向通知的方式是自定義通知隊列(不是
NSNotificationQueue
對象),讓自定義隊列去維護需要重定向的通知。仍然像之前一樣注冊通知的觀察者,當接收到通知時,判斷當前線程是否是我們期望的線程,如果不是,就將通知放到自定義隊列中,然后發送一個信號sigal
到期望的線程中,告知這個線程需要處理通知。指定線程收到通知后,從自定義隊列中把這個通知移除,并進行后續處理。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self setUpThreadingSupport];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:NOTIFICATION_NAME object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Post Notification. Current thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:nil];
});
}
- (void)setUpThreadingSupport {
if (self.notifications) {
return;
}
self.notifications = [NSMutableArray array];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
[self.notificationPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:self.notificationPort forMode:(__bridge NSString *)kCFRunLoopCommonModes];
}
- (void)processNotification:(NSNotification *)n {
if ([NSThread currentThread] != self.notificationThread) {
[self.notificationLock lock];
[self.notifications addObject:n];
[self.notificationLock unlock];
[self.notificationPort sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];
} else {
// Process the notification here;
NSLog(@"Receive Notification. Current thread = %@", [NSThread currentThread]);
}
}
#pragma MacPort Delegate
- (void)handleMachMessage:(void *)msg {
[self.notificationLock lock];
while (self.notifications.count) {
NSNotification *n = [self.notifications objectAtIndex:0];
[self.notifications removeObjectAtIndex:0];
[self.notificationLock unlock];
[self processNotification:n];
[self.notificationLock lock];
}
[self.notificationLock unlock];
}
輸出結果:
Post Notification. Current thread = <NSThread: 0x600002641300>{number = 3, name = (null)}
Receive Notification. Current thread = <NSThread: 0x60000261e940>{number = 1, name = main}
從運行結果可知,在子線程發送通知,在主線程接收和處理通知。當然這種實現方式也有限制:
- 所有線程的通知處理都必須通過相同的方法
processNotification:
- 每個對象必須提供自己的實現和通信端口
通知的實現原理
NSNotification
是一個類蔟不能夠實例化,當我們調用initWithName:object:userInfo:
方法的時候,系統內部會實例化NSNotification
的子類NSConcreteNotification
。在這個子類中重寫了NSNofication
定義的相關字段和方法。
NSNotificationCenter
是通知的管理類,實現較復雜。NSNotificationCenter
中主要定義了兩個table,同時也定義了Observation
保存觀察者信息。它們結構體可以簡化如下:
typedef struct NCTbl {
Observation *wildcard; /* Get ALL messages. */
GSIMapTable nameless; /* Get messages for any name. */
GSIMapTable named; /* Getting named messages only. */
} NCTable;
typedef struct Obs {
id observer; /* Object to receive message. */
SEL selector; /* Method selector. */
struct Obs *next; /* Next item in linked list. */
int retained; /* Retain count for structure. */
struct NCTbl *link; /* Pointer back to chunk table */
} Observation;
在NSNotificationCenter
內部保存了兩張表:一張用戶保存添加觀察者時傳入了NotificationName的情況,一種用戶保存添加觀察者時沒有傳入NoficationName的情況。
Named Table
在named table中,NotificationName作為表的key,但因注冊觀察者的時可傳入一個object參數用于接收指定對象發出的通知,并且一個通知可注冊多個觀察者,所以還需要一張表來保存object和observer的對應關系。這張表以object為key,observer為value。如何實現同一個通知保存多個觀察者的情況?答案就是用鏈表的數據結構。
named table最終的數據結構如上圖所示:
- 外層是一個table,以通知名稱NotificationName為key,其value為一個table(簡稱內層table)。
- 內層table以object為key,其value為一個鏈表,用來保存所有的觀察者。
注意: 在實際開發過程中object參數我們經常傳nil,這時候系統會根據nil自動生成一個key,相當于這個key對應的value(鏈表)保存的就是當前通知傳入了NotificationName沒有傳入object的所有觀察者。當對應的NotificationName的通知發送時,鏈表中所有的觀察者都會收到通知。
Nameless Table
Nameless Table比Named Table要簡單很多,因為沒有NotificationName作為key,直接用object作為key。相較于Named Table要少一層table嵌套。
wildcard
wildcard是鏈表的數據結構,如果在注冊觀察者時既沒有傳入NotificationName,也沒有傳入object,就會添加到wildcard的鏈表中。注冊到這里的觀察者能接收到 所有的系統通知。
添加觀察者流程
有了上面基本的結構關系,再來看添加過程就會很簡單。在初始化NotificationCenter時會創建一個對象,這個對象里保存了Named Table、Nameless Table、wildcard和一些其它信息。所有注冊觀察者的操作最后都會調用addObserver:selector:name:object:
。
- 首先會根據傳入的參數實例化一個Observation,Observation對象保存了觀察者對象,接收到通知觀察者所執行的方法,以及下一個Observation對象的地址。
- 根據是否傳入NotificationName選擇操作Named Table還是Nameless Table。
- 若傳入了NotificationName,則會以NotificationName為key去查找對應的Value,若找到value,則取出對應的value;若未找到對應的value,則新建一個table,然后將這個table以NotificationName為key添加到Named Table中。
- 若在保存Observation的table中,以object為key取對應的鏈表。若找到了則直接在鏈接末尾插入之前實例化好的Observation;若未找到則以之前實例化好的Observation對象作為頭節點插入進去。
沒有傳入NotificationName的情況和上面的過程類似,只不過是直接根據對應的object為key去找對應的鏈表而已。如果既沒有傳入NotificationName也沒有傳入object,則這個觀察者會添加到wildcard鏈表中。
發送通知流程
發送通知一般調用postNotificationName:object:userInfo:
來實現,內部會根據傳入的參數實例化一個NSNotification對象,包含name、object、userinfo等信息。
發送通知的流程總體來說是根據NotificationName和object找到對應的鏈表,然后遍歷整個鏈表,給每個Observation節點中保存的oberver發送對應的SEL消息。
- 首先會創建一個數組observerArray用來保存需要通知的observer。
- 遍歷wildcard鏈表,將observer添加到observerArray數組中。
- 若存在object,在nameless table中找到以object為key的鏈表,然后遍歷找到的鏈表,將observer添加到observerArray數組中。
- 若存在NotificationName,在named table中以NotificationName為key找到對應的table,然后再在找到的table中以object為key找到對應的鏈表,遍歷鏈表,將observer添加到observerArray數組中。如果object不為nil,則以nil為key找到對應的鏈表,遍歷鏈表,將observer添加到observerArray數組中。
- 至此所有關于當前通知的observer(wildcard+nameless+named)都已經加入到了數組observerArray中。遍歷observerArray數組,取出其中的observer節點(包含了觀察者對象和selector),調用形式如下:
[o->observer performSelector: o->selector withObject: notification];
這種處理通知的方式也就能說明,發送通知的線程和接收通知的線程是同一線程。
移除通知流程
根據前面分析的添加觀察者的流程與發送通知的流程可以類比出移除通知的流程。
- 若NotificationName和object都為nil,則清空wildcard鏈表。
- 若NotificationName為nil,遍歷named table,若object為nil,則清空named table,若object不為nil,則以object為key找到對應的鏈表,然后清空鏈表。在nameless table中以object為key找到對應的observer鏈表,然后清空,若object也為nil,則清空nameless table。
- 若NotificationName不為nil,在named table中以NotificationName為key找到對應的table,若object為nil,則清空找到的table,若object不為nil,則以object為key在找到的table中取出對應的鏈表,然后清空鏈表。
總結
其實上述分析通知的過程中仍有很多細節沒有考慮到,比如在整個table非常大的時候是如何保證查詢效率的。感興趣的同學可以進行更深層次的研究。
參考文獻
Notification
NSNotificationCenter
NSNotificationQueue
Delivering Notifications To Particular Threads
Gunstep NSNotififcationCenter sourcecode