簡(jiǎn)述
本文主要是針對(duì)iOS通知機(jī)制的全面解析,從接口到原理面面俱到。同時(shí)也解決了之前寫的文章阿里、字節(jié):一套高效的iOS面試題中關(guān)于通知的問題,相信看完此文再也不怕面試官問我任何通知相關(guān)問題了
由于蘋果沒有對(duì)相關(guān)源碼開放,所以以GNUStep源碼為基礎(chǔ)進(jìn)行研究,GNUStep雖然不是蘋果官方的源碼,但很具有參考意義,根據(jù)實(shí)現(xiàn)原理來猜測(cè)和實(shí)踐,更重要的還可以學(xué)習(xí)觀察者模式的架構(gòu)設(shè)計(jì)
問題列表
先把之前的問題列出來,詳細(xì)讀完本文之后,你會(huì)找到答案
- 實(shí)現(xiàn)原理(結(jié)構(gòu)設(shè)計(jì)、通知如何存儲(chǔ)的、
name&observer&SEL之間的關(guān)系等) - 通知的發(fā)送時(shí)同步的,還是異步的
-
NSNotificationCenter接受消息和發(fā)送消息是在一個(gè)線程里嗎?如何異步發(fā)送消息 -
NSNotificationQueue是異步還是同步發(fā)送?在哪個(gè)線程響應(yīng) -
NSNotificationQueue和runloop的關(guān)系 - 如何保證通知接收的線程在主線程
- 頁面銷毀時(shí)不移除通知會(huì)崩潰嗎
- 多次添加同一個(gè)通知會(huì)是什么結(jié)果?多次移除通知呢
- 下面的方式能接收到通知嗎?為什么
// 發(fā)送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
復(fù)制代碼
關(guān)鍵類結(jié)構(gòu)
NSNotification
用于描述通知的類,一個(gè)NSNotification對(duì)象就包含了一條通知的信息,所以當(dāng)創(chuàng)建一個(gè)通知時(shí)通常包含如下屬性:
@interface NSNotification : NSObject <NSCopying, NSCoding>
...
/* Querying a Notification Object */
- (NSString*) name; // 通知的name
- (id) object; // 攜帶的對(duì)象
- (NSDictionary*) userInfo; // 配置信息
@end
復(fù)制代碼
作為一個(gè)開發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)我的iOS交流群:761407670 進(jìn)群密碼000,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗(yàn),討論技術(shù), 大家一起交流學(xué)習(xí)成長!
提供逆向安防、Swift、算法、架構(gòu)設(shè)計(jì)、多線程,網(wǎng)絡(luò)進(jìn)階,還有底層、音視頻、Flutter等資料

一般用于發(fā)送通知時(shí)使用,常用api如下:
- (void)postNotification:(NSNotification *)notification;
復(fù)制代碼
NSNotificationCenter
這是個(gè)單例類,負(fù)責(zé)管理通知的創(chuàng)建和發(fā)送,屬于最核心的類了。而NSNotificationCenter類主要負(fù)責(zé)三件事
- 添加通知
- 發(fā)送通知
- 移除通知
核心API如下:
// 添加通知
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 發(fā)送通知
- (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;
復(fù)制代碼
NSNotificationQueue
功能介紹
通知隊(duì)列,用于異步發(fā)送消息,這個(gè)異步并不是開啟線程,而是把通知存到雙向鏈表實(shí)現(xiàn)的隊(duì)列里面,等待某個(gè)時(shí)機(jī)觸發(fā)時(shí)調(diào)用NSNotificationCenter的發(fā)送接口進(jìn)行發(fā)送通知,這么看NSNotificationQueue最終還是調(diào)用NSNotificationCenter進(jìn)行消息的分發(fā)
另外NSNotificationQueue是依賴runloop的,所以如果線程的runloop未開啟則無效,至于為什么依賴runloop下面會(huì)解釋
NSNotificationQueue主要做了兩件事:
- 添加通知到隊(duì)列
- 刪除通知
核心API如下:
// 把通知添加到隊(duì)列中,NSPostingStyle是個(gè)枚舉,下面會(huì)介紹
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 刪除通知,把滿足合并條件的通知從隊(duì)列中刪除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
復(fù)制代碼
隊(duì)列的合并策略和發(fā)送時(shí)機(jī)
把通知添加到隊(duì)列等待發(fā)送,同時(shí)提供了一些附加條件供開發(fā)者選擇,如:什么時(shí)候發(fā)送通知、如何合并通知等,系統(tǒng)給了如下定義
// 表示通知的發(fā)送時(shí)機(jī)
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // runloop空閑時(shí)發(fā)送通知
NSPostASAP = 2, // 盡快發(fā)送,這種情況稍微復(fù)雜,這種時(shí)機(jī)是穿插在每次事件完成期間來做的
NSPostNow = 3 // 立刻發(fā)送或者合并通知完成之后發(fā)送
};
// 通知合并的策略,有些時(shí)候同名通知只想存在一個(gè),這時(shí)候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0, // 默認(rèn)不合并
NSNotificationCoalescingOnName = 1, // 只要name相同,就認(rèn)為是相同通知
NSNotificationCoalescingOnSender = 2 // object相同
};
復(fù)制代碼
GSNotificationObserver
這個(gè)類是GNUStep源碼中定義的,它的作用是代理觀察者,主要用來實(shí)現(xiàn)接口:addObserverForName:object: queue: usingBlock:時(shí)用到,即要實(shí)現(xiàn)在指定隊(duì)列回調(diào)block,那么GSNotificationObserver對(duì)象保存了queue和block信息,并且作為觀察者注冊(cè)到通知中心,等到接收通知時(shí)觸發(fā)了響應(yīng)方法,并在響應(yīng)方法中把block拋到指定queue中執(zhí)行,定義如下:
@implementation GSNotificationObserver
{
NSOperationQueue *_queue; // 保存?zhèn)魅氲年?duì)列
GSNotificationBlock _block; // 保存?zhèn)魅氲腷lock
}
- (id) initWithQueue: (NSOperationQueue *)queue
block: (GSNotificationBlock)block
{
......初始化操作
}
- (void) dealloc
{
....
}
// 響應(yīng)接收通知的方法,并在指定隊(duì)列中執(zhí)行block
- (void) didReceiveNotification: (NSNotification *)notif
{
if (_queue != nil)
{
GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc]
initWithNotification: notif block: _block];
[_queue addOperation: op];
}
else
{
CALL_BLOCK(_block, notif);
}
}
@end
復(fù)制代碼
存儲(chǔ)容器
上面介紹了一些類的功能,但是要想實(shí)現(xiàn)通知中心的邏輯必須設(shè)計(jì)一套合理的存儲(chǔ)結(jié)構(gòu),對(duì)于通知的存儲(chǔ)基本上圍繞下面幾個(gè)結(jié)構(gòu)體來做(大致了解下,后面章節(jié)會(huì)用到),后面會(huì)詳細(xì)介紹具體邏輯的
// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
Observation *wildcard; /* 鏈表結(jié)構(gòu),保存既沒有name也沒有object的通知 */
GSIMapTable nameless; /* 存儲(chǔ)沒有name但是有object的通知 */
GSIMapTable named; /* 存儲(chǔ)帶有name的通知,不管有沒有object */
...
} NCTable;
// Observation 存儲(chǔ)觀察者和響應(yīng)結(jié)構(gòu)體,基本的存儲(chǔ)單元
typedef struct Obs {
id observer; /* 觀察者,接收通知的對(duì)象 */
SEL selector; /* 響應(yīng)方法 */
struct Obs *next; /* Next item in linked list. */
...
} Observation;
復(fù)制代碼
注冊(cè)通知
正式開始“注冊(cè)通知”的深入研究,注冊(cè)通知有幾個(gè)常用方法,但只需要研究典型的一兩個(gè)就夠了,原理都是一樣的
目前只介紹NSNotificationCenter的注冊(cè)流程,NSNotificationQueue的方式在下面章節(jié)單獨(dú)拎出來解釋
接口1
直接看源碼(精簡(jiǎn)版便于理解)
/*
observer:觀察者,即通知的接收者
selector:接收到通知時(shí)的響應(yīng)方法
name: 通知name
object:攜帶對(duì)象
*/
- (void) addObserver: (id)observer
selector: (SEL)selector
name: (NSString*)name
object: (id)object {
// 前置條件判斷
......
// 創(chuàng)建一個(gè)observation對(duì)象,持有觀察者和SEL,下面進(jìn)行的所有邏輯就是為了存儲(chǔ)它
o = obsNew(TABLE, selector, observer);
/*======= case1: 如果name存在 =======*/
if (name) {
//-------- NAMED是個(gè)宏,表示名為named字典。以name為key,從named表中獲取對(duì)應(yīng)的mapTable
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
if (n == 0) { // 不存在,則創(chuàng)建
m = mapNew(TABLE); // 先取緩存,如果緩存沒有則新建一個(gè)map
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
...
}
else { // 存在則把值取出來 賦值給m
m = (GSIMapTable)n->value.ptr;
}
//-------- 以object為key,從字典m中取出對(duì)應(yīng)的value,其實(shí)value被MapNode的結(jié)構(gòu)包裝了一層,這里不追究細(xì)節(jié)
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n == 0) {// 不存在,則創(chuàng)建
o->next = ENDOBS;
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
}
else {
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
}
/*======= case2:如果name為空,但object不為空 =======*/
else if (object) {
// 以object為key,從nameless字典中取出對(duì)應(yīng)的value,value是個(gè)鏈表結(jié)構(gòu)
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
// 不存在則新建鏈表,并存到map中
if (n == 0) {
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}
else { // 存在 則把值接到鏈表的節(jié)點(diǎn)上
...
}
}
/*======= case3:name 和 object 都為空 則存儲(chǔ)到wildcard鏈表中 =======*/
else {
o->next = WILDCARD;
WILDCARD = o;
}
}
復(fù)制代碼
邏輯說明
從上面介紹的存儲(chǔ)容器中我們了解到NCTable結(jié)構(gòu)體中核心的三個(gè)變量以及功能:wildcard、named、nameless,在源碼中直接用宏定義表示了:WILDCARD、NAMELESS、NAMED,下面邏輯會(huì)用到
建議如果看文字說明覺得復(fù)雜不好理解,就看看下節(jié)介紹的存儲(chǔ)關(guān)系圖
case1: 存在name(無論object是否存在)
- 注冊(cè)通知,如果通知的
name存在,則以name為key從named字典中取出值n(這個(gè)n其實(shí)被MapNode包裝了一層,便于理解這里直接認(rèn)為沒有包裝),這個(gè)n還是個(gè)字典,各種判空新建邏輯不討論 - 然后以
object為key,從字典n中取出對(duì)應(yīng)的值,這個(gè)值就是Observation類型(后面簡(jiǎn)稱obs)的鏈表,然后把剛開始創(chuàng)建的obs對(duì)象o存儲(chǔ)進(jìn)去
數(shù)據(jù)結(jié)構(gòu)關(guān)系圖
這里就回答了上述問題列表的問題1的一部分,現(xiàn)在梳理下存儲(chǔ)關(guān)系

如果注冊(cè)通知時(shí)傳入name,那么會(huì)是一個(gè)雙層的存儲(chǔ)結(jié)構(gòu)
- 找到
NCTable中的named表,這個(gè)表存儲(chǔ)了還有name的通知 - 以
name作為key,找到value,這個(gè)value依然是一個(gè)map -
map的結(jié)構(gòu)是以object作為key,obs對(duì)象為value,這個(gè)obs對(duì)象的結(jié)構(gòu)上面已經(jīng)解釋,主要存儲(chǔ)了observer & SEL
case2: 只存在object
- 以
object為key,從nameless字典中取出value,此value是個(gè)obs類型的鏈表 - 把創(chuàng)建的
obs類型的對(duì)象o存儲(chǔ)到鏈表中
數(shù)據(jù)結(jié)構(gòu)關(guān)系圖

只存在object時(shí)存儲(chǔ)只有一層,那就是object和obs對(duì)象之間的映射
case3: 沒有name和object
這種情況直接把obs對(duì)象存放在了Observation *wildcard 鏈表結(jié)構(gòu)中
接口2
源碼
接口功能: 此接口實(shí)現(xiàn)的功能是在接收到通知時(shí),在指定隊(duì)列queue執(zhí)行block
// 這個(gè)api使用頻率較低,怎么實(shí)現(xiàn)在指定隊(duì)列回調(diào)block的,值得研究
- (id) addObserverForName: (NSString *)name
object: (id)object
queue: (NSOperationQueue *)queue
usingBlock: (GSNotificationBlock)block
{
// 創(chuàng)建一個(gè)臨時(shí)觀察者
GSNotificationObserver *observer =
[[GSNotificationObserver alloc] initWithQueue: queue block: block];
// 調(diào)用了接口1的注冊(cè)方法
[self addObserver: observer
selector: @selector(didReceiveNotification:)
name: name
object: object];
return observer;
}
復(fù)制代碼
邏輯說明
這個(gè)接口依賴于接口1,只是多了一層代理觀察者GSNotificationObserver,在關(guān)鍵類結(jié)構(gòu)中已經(jīng)介紹了它,設(shè)計(jì)思路值得學(xué)習(xí)
- 創(chuàng)建一個(gè)
GSNotificationObserver類型的對(duì)象observer,并把queue和block保存下來 - 調(diào)用接口1進(jìn)行通知的注冊(cè)
- 接收到通知時(shí)會(huì)響應(yīng)
observer的didReceiveNotification:方法,然后在didReceiveNotification:中把block拋給指定的queue去執(zhí)行
小結(jié)
- 從上述介紹可以總結(jié),存儲(chǔ)是以
name和object為維度的,即判定是不是同一個(gè)通知要從name和object區(qū)分,如果他們都相同則認(rèn)為是同一個(gè)通知,后面包括查找邏輯、刪除邏輯都是以這兩個(gè)為維度的,問題列表中的第九題也迎刃而解了 - 理解數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)是整個(gè)通知機(jī)制的核心,其他功能只是在此基礎(chǔ)上擴(kuò)展了一些邏輯
- 存儲(chǔ)過程并沒有做去重操作,這也解釋了為什么同一個(gè)通知注冊(cè)多次則響應(yīng)多次
發(fā)送通知
源碼
發(fā)送通知的核心邏輯比較簡(jiǎn)單,基本上就是查找和調(diào)用響應(yīng)方法,核心函數(shù)如下
// 發(fā)送通知
- (void) postNotificationName: (NSString*)name
object: (id)object
userInfo: (NSDictionary*)info
{
// 構(gòu)造一個(gè)GSNotification對(duì)象, GSNotification繼承了NSNotification
GSNotification *notification;
notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
notification->_name = [name copyWithZone: [self zone]];
notification->_object = [object retain];
notification->_info = [info retain];
// 進(jìn)行發(fā)送操作
[self _postAndRelease: notification];
}
//發(fā)送通知的核心函數(shù),主要做了三件事:查找通知、發(fā)送、釋放資源
- (void) _postAndRelease: (NSNotification*)notification {
//step1: 從named、nameless、wildcard表中查找對(duì)應(yīng)的通知
...
//step2:執(zhí)行發(fā)送,即調(diào)用performSelector執(zhí)行響應(yīng)方法,從這里可以看出是同步的
[o->observer performSelector: o->selector
withObject: notification];
//step3: 釋放資源
RELEASE(notification);
}
復(fù)制代碼
邏輯說明
其實(shí)上述代碼注釋說的很清晰了,主要做了三件事
- 通過
name & object查找到所有的obs對(duì)象(保存了observer和sel),放到數(shù)組中 - 通過
performSelector:逐一調(diào)用sel,這是個(gè)同步操作 - 釋放
notification對(duì)象
小結(jié)
從源碼邏輯可以看出發(fā)送過程的概述:從三個(gè)存儲(chǔ)容器中:named、nameless、wildcard去查找對(duì)應(yīng)的obs對(duì)象,然后通過performSelector:逐一調(diào)用響應(yīng)方法,這就完成了發(fā)送流程
核心點(diǎn):
- 同步發(fā)送
- 遍歷所有列表,即注冊(cè)多次通知就會(huì)響應(yīng)多次
刪除通知
這里源碼太長而且基本上都是查找刪除邏輯,不一一列舉,感興趣的去下載源碼看下吧
要注意的點(diǎn):
- 查找時(shí)仍然以
name和object為維度的,再加上observer做區(qū)分 - 因?yàn)椴檎視r(shí)做了這個(gè)鏈表的遍歷,所以刪除時(shí)會(huì)把重復(fù)的通知全都刪除掉
// 刪除已經(jīng)注冊(cè)的通知
- (void) removeObserver: (id)observer
name: (NSString*)name
object: (id)object {
if (name == nil && object == nil && observer == nil)
return;
...
}
- (void) removeObserver: (id)observer
{
if (observer == nil)
return;
[self removeObserver: observer name: nil object: nil];
}
復(fù)制代碼
異步通知
上面介紹的NSNotificationCenter都是同步發(fā)送的,而這里介紹關(guān)于NSNotificationQueue的異步發(fā)送,從線程的角度看并不是真正的異步發(fā)送,或可稱為延時(shí)發(fā)送,它是利用了runloop的時(shí)機(jī)來觸發(fā)的
入隊(duì)
下面為精簡(jiǎn)版的源碼,看源碼的注釋,基本上能明白大致邏輯
- 根據(jù)
coalesceMask參數(shù)判斷是否合并通知 - 接著根據(jù)
postingStyle參數(shù),判斷通知發(fā)送的時(shí)機(jī),如果不是立即發(fā)送則把通知加入到隊(duì)列中:_asapQueue、_idleQueue
核心點(diǎn):
- 隊(duì)列是雙向鏈表實(shí)現(xiàn)
- 當(dāng)postingStyle值是立即發(fā)送時(shí),調(diào)用的是
NSNotificationCenter進(jìn)行發(fā)送的,所以NSNotificationQueue還是依賴NSNotificationCenter進(jìn)行發(fā)送
/*
* 把要發(fā)送的通知添加到隊(duì)列,等待發(fā)送
* NSPostingStyle 和 coalesceMask在上面的類結(jié)構(gòu)中有介紹
* modes這個(gè)就和runloop有關(guān)了,指的是runloop的mode
*/
- (void) enqueueNotification: (NSNotification*)notification
postingStyle: (NSPostingStyle)postingStyle
coalesceMask: (NSUInteger)coalesceMask
forModes: (NSArray*)modes
{
......
// 判斷是否需要合并通知
if (coalesceMask != NSNotificationNoCoalescing) {
[self dequeueNotificationsMatching: notification
coalesceMask: coalesceMask];
}
switch (postingStyle) {
case NSPostNow: {
...
// 如果是立馬發(fā)送,則調(diào)用NSNotificationCenter進(jìn)行發(fā)送
[_center postNotification: notification];
break;
}
case NSPostASAP:
// 添加到_asapQueue隊(duì)列,等待發(fā)送
add_to_queue(_asapQueue, notification, modes, _zone);
break;
case NSPostWhenIdle:
// 添加到_idleQueue隊(duì)列,等待發(fā)送
add_to_queue(_idleQueue, notification, modes, _zone);
break;
}
}
復(fù)制代碼
發(fā)送通知
這里截取了發(fā)送通知的核心代碼,這個(gè)發(fā)送通知邏輯如下:
-
runloop觸發(fā)某個(gè)時(shí)機(jī),調(diào)用GSPrivateNotifyASAP()和GSPrivateNotifyIdle()方法,這兩個(gè)方法最終都調(diào)用了notify()方法 -
notify()所做的事情就是調(diào)用NSNotificationCenter的postNotification:進(jìn)行發(fā)送通知
static void notify(NSNotificationCenter *center,
NSNotificationQueueList *list,
NSString *mode, NSZone *zone)
{
......
// 循環(huán)遍歷發(fā)送通知
for (pos = 0; pos < len; pos++)
{
NSNotification *n = (NSNotification*)ptr[pos];
[center postNotification: n];
RELEASE(n);
}
......
}
// 發(fā)送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
notify(item->queue->_center,
item->queue->_asapQueue,
mode,
item->queue->_zone);
}
// 發(fā)送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
notify(item->queue->_center,
item->queue->_idleQueue,
mode,
item->queue->_zone);
}
復(fù)制代碼
小結(jié)
對(duì)于NSNotificationQueue總結(jié)如下
- 依賴
runloop,所以如果在其他子線程使用NSNotificationQueue,需要開啟runloop - 最終還是通過
NSNotificationCenter進(jìn)行發(fā)送通知,所以這個(gè)角度講它還是同步的 - 所謂異步,指的是非實(shí)時(shí)發(fā)送而是在合適的時(shí)機(jī)發(fā)送,并沒有開啟異步線程
主線程響應(yīng)通知
異步線程發(fā)送通知?jiǎng)t響應(yīng)函數(shù)也是在異步線程,如果執(zhí)行UI刷新相關(guān)的話就會(huì)出問題,那么如何保證在主線程響應(yīng)通知呢?
其實(shí)也是比較常見的問題了,基本上解決方式如下幾種:
- 使用
addObserverForName: object: queue: usingBlock方法注冊(cè)通知,指定在mainqueue上響應(yīng)block - 在主線程注冊(cè)一個(gè)
machPort,它是用來做線程通信的,當(dāng)在異步線程收到通知,然后給machPort發(fā)送消息,這樣肯定是在主線程處理的,具體用法去網(wǎng)上資料很多,蘋果官網(wǎng)也有
總結(jié)
本文寫的內(nèi)容比較多,以GNUStep源碼為基礎(chǔ)進(jìn)行研究,全面闡述了通知的存儲(chǔ)、發(fā)送、異步發(fā)送等原理,對(duì)研究學(xué)習(xí)有很大幫助
最后推薦個(gè)我的高級(jí)iOS交流群:761407670 進(jìn)群密碼000,有一個(gè)共同的圈子很重要,結(jié)識(shí)人脈!里面都是iOS開發(fā),全棧發(fā)展,歡迎入駐,共同進(jìn)步!