簡述
本文主要是針對iOS通知機制
的全面解析储玫,從接口到原理面面俱到岩睁。同時也解決了之前寫的文章阿里藕筋、字節(jié):一套高效的iOS面試題中關(guān)于通知的問題翅雏,相信看完此文再也不怕面試官問我任何通知相關(guān)問題了
由于蘋果沒有對相關(guān)源碼開放圈驼,所以以GNUStep源碼為基礎(chǔ)進行研究,GNUStep雖然不是蘋果官方的源碼枚荣,但很具有參考意義碗脊,根據(jù)實現(xiàn)原理來猜測和實踐,更重要的還可以學(xué)習(xí)觀察者模式的架構(gòu)設(shè)計
問題列表
先把之前的問題列出來,詳細(xì)讀完本文之后衙伶,你會找到答案
- 實現(xiàn)原理(結(jié)構(gòu)設(shè)計祈坠、通知如何存儲的、
name&observer&SEL
之間的關(guān)系等) - 通知的發(fā)送時同步的矢劲,還是異步的
-
NSNotificationCenter
接受消息和發(fā)送消息是在一個線程里嗎赦拘?如何異步發(fā)送消息 -
NSNotificationQueue
是異步還是同步發(fā)送?在哪個線程響應(yīng) -
NSNotificationQueue
和runloop
的關(guān)系 - 如何保證通知接收的線程在主線程
- 頁面銷毀時不移除通知會崩潰嗎
- 多次添加同一個通知會是什么結(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
用于描述通知的類,一個NSNotification
對象就包含了一條通知的信息丸逸,所以當(dāng)創(chuàng)建一個通知時通常包含如下屬性:
@interface NSNotification : NSObject <NSCopying, NSCoding>
...
/* Querying a Notification Object */
- (NSString*) name; // 通知的name
- (id) object; // 攜帶的對象
- (NSDictionary*) userInfo; // 配置信息
@end
復(fù)制代碼
作為一個開發(fā)者蹋艺,有一個學(xué)習(xí)的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:761407670 進群密碼000黄刚,不管你是小白還是大牛歡迎入駐 捎谨,分享BAT,阿里面試題、面試經(jīng)驗憔维,討論技術(shù)涛救, 大家一起交流學(xué)習(xí)成長!
提供逆向安防业扒、Swift检吆、算法、架構(gòu)設(shè)計程储、多線程蹭沛,網(wǎng)絡(luò)進階,還有底層虱肄、音視頻致板、Flutter等資料
一般用于發(fā)送通知時使用交煞,常用api如下:
- (void)postNotification:(NSNotification *)notification;
復(fù)制代碼
NSNotificationCenter
這是個單例類咏窿,負(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
功能介紹
通知隊列集嵌,用于異步發(fā)送消息,這個異步并不是開啟線程御毅,而是把通知存到雙向鏈表實現(xiàn)的隊列里面根欧,等待某個時機觸發(fā)時調(diào)用NSNotificationCenter
的發(fā)送接口進行發(fā)送通知,這么看NSNotificationQueue
最終還是調(diào)用NSNotificationCenter
進行消息的分發(fā)
另外NSNotificationQueue
是依賴runloop
的端蛆,所以如果線程的runloop
未開啟則無效凤粗,至于為什么依賴runloop
下面會解釋
NSNotificationQueue
主要做了兩件事:
- 添加通知到隊列
- 刪除通知
核心API
如下:
// 把通知添加到隊列中,NSPostingStyle是個枚舉今豆,下面會介紹
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 刪除通知嫌拣,把滿足合并條件的通知從隊列中刪除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
復(fù)制代碼
隊列的合并策略和發(fā)送時機
把通知添加到隊列等待發(fā)送柔袁,同時提供了一些附加條件供開發(fā)者選擇,如:什么時候發(fā)送通知异逐、如何合并通知等捶索,系統(tǒng)給了如下定義
// 表示通知的發(fā)送時機
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // runloop空閑時發(fā)送通知
NSPostASAP = 2, // 盡快發(fā)送,這種情況稍微復(fù)雜灰瞻,這種時機是穿插在每次事件完成期間來做的
NSPostNow = 3 // 立刻發(fā)送或者合并通知完成之后發(fā)送
};
// 通知合并的策略腥例,有些時候同名通知只想存在一個,這時候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0, // 默認(rèn)不合并
NSNotificationCoalescingOnName = 1, // 只要name相同酝润,就認(rèn)為是相同通知
NSNotificationCoalescingOnSender = 2 // object相同
};
復(fù)制代碼
GSNotificationObserver
這個類是GNUStep源碼中定義的燎竖,它的作用是代理觀察者,主要用來實現(xiàn)接口:addObserverForName:object: queue: usingBlock:
時用到要销,即要實現(xiàn)在指定隊列回調(diào)block底瓣,那么GSNotificationObserver
對象保存了queue
和block
信息,并且作為觀察者注冊到通知中心蕉陋,等到接收通知時觸發(fā)了響應(yīng)方法捐凭,并在響應(yīng)方法中把block
拋到指定queue
中執(zhí)行,定義如下:
@implementation GSNotificationObserver
{
NSOperationQueue *_queue; // 保存?zhèn)魅氲年犃? GSNotificationBlock _block; // 保存?zhèn)魅氲腷lock
}
- (id) initWithQueue: (NSOperationQueue *)queue
block: (GSNotificationBlock)block
{
......初始化操作
}
- (void) dealloc
{
....
}
// 響應(yīng)接收通知的方法凳鬓,并在指定隊列中執(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ù)制代碼
存儲容器
上面介紹了一些類的功能茁肠,但是要想實現(xiàn)通知中心的邏輯必須設(shè)計一套合理的存儲結(jié)構(gòu),對于通知的存儲基本上圍繞下面幾個結(jié)構(gòu)體來做(大致了解下缩举,后面章節(jié)會用到)垦梆,后面會詳細(xì)介紹具體邏輯的
// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
Observation *wildcard; /* 鏈表結(jié)構(gòu)仅孩,保存既沒有name也沒有object的通知 */
GSIMapTable nameless; /* 存儲沒有name但是有object的通知 */
GSIMapTable named; /* 存儲帶有name的通知托猩,不管有沒有object */
...
} NCTable;
// Observation 存儲觀察者和響應(yīng)結(jié)構(gòu)體,基本的存儲單元
typedef struct Obs {
id observer; /* 觀察者辽慕,接收通知的對象 */
SEL selector; /* 響應(yīng)方法 */
struct Obs *next; /* Next item in linked list. */
...
} Observation;
復(fù)制代碼
注冊通知
正式開始“注冊通知”的深入研究京腥,注冊通知有幾個常用方法,但只需要研究典型的一兩個就夠了溅蛉,原理都是一樣的
目前只介紹NSNotificationCenter
的注冊流程公浪,NSNotificationQueue
的方式在下面章節(jié)單獨拎出來解釋
接口1
直接看源碼(精簡版便于理解)
/*
observer:觀察者,即通知的接收者
selector:接收到通知時的響應(yīng)方法
name: 通知name
object:攜帶對象
*/
- (void) addObserver: (id)observer
selector: (SEL)selector
name: (NSString*)name
object: (id)object {
// 前置條件判斷
......
// 創(chuàng)建一個observation對象船侧,持有觀察者和SEL欠气,下面進行的所有邏輯就是為了存儲它
o = obsNew(TABLE, selector, observer);
/*======= case1: 如果name存在 =======*/
if (name) {
//-------- NAMED是個宏,表示名為named字典镜撩。以name為key预柒,從named表中獲取對應(yīng)的mapTable
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
if (n == 0) { // 不存在,則創(chuàng)建
m = mapNew(TABLE); // 先取緩存,如果緩存沒有則新建一個map
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
...
}
else { // 存在則把值取出來 賦值給m
m = (GSIMapTable)n->value.ptr;
}
//-------- 以object為key宜鸯,從字典m中取出對應(yīng)的value人灼,其實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字典中取出對應(yīng)的value适贸,value是個鏈表結(jié)構(gòu)
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
// 不存在則新建鏈表灸芳,并存到map中
if (n == 0) {
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}
else { // 存在 則把值接到鏈表的節(jié)點上
...
}
}
/*======= case3:name 和 object 都為空 則存儲到wildcard鏈表中 =======*/
else {
o->next = WILDCARD;
WILDCARD = o;
}
}
復(fù)制代碼
邏輯說明
從上面介紹的存儲容器中我們了解到NCTable
結(jié)構(gòu)體中核心的三個變量以及功能:wildcard
、named
拜姿、nameless
烙样,在源碼中直接用宏定義表示了:WILDCARD
、NAMELESS
蕊肥、NAMED
谒获,下面邏輯會用到
建議如果看文字說明覺得復(fù)雜不好理解,就看看下節(jié)介紹的存儲關(guān)系圖
case1: 存在name
(無論object是否存在)
- 注冊通知壁却,如果通知的
name
存在批狱,則以name
為key從named
字典中取出值n
(這個n
其實被MapNode
包裝了一層,便于理解這里直接認(rèn)為沒有包裝)展东,這個n
還是個字典赔硫,各種判空新建邏輯不討論 - 然后以
object
為key,從字典n
中取出對應(yīng)的值盐肃,這個值就是Observation
類型(后面簡稱obs
)的鏈表爪膊,然后把剛開始創(chuàng)建的obs
對象o
存儲進去
數(shù)據(jù)結(jié)構(gòu)關(guān)系圖
這里就回答了上述問題列表的問題1的一部分,現(xiàn)在梳理下存儲關(guān)系
如果注冊通知時傳入name
砸王,那么會是一個雙層的存儲結(jié)構(gòu)
- 找到
NCTable
中的named
表推盛,這個表存儲了還有name
的通知 - 以
name
作為key,找到value
谦铃,這個value
依然是一個map
-
map
的結(jié)構(gòu)是以object
作為key耘成,obs
對象為value,這個obs
對象的結(jié)構(gòu)上面已經(jīng)解釋荷辕,主要存儲了observer & SEL
case2: 只存在object
- 以
object
為key凿跳,從nameless
字典中取出value,此value是個obs
類型的鏈表 - 把創(chuàng)建的
obs
類型的對象o
存儲到鏈表中
數(shù)據(jù)結(jié)構(gòu)關(guān)系圖
只存在object
時存儲只有一層疮方,那就是object
和obs
對象之間的映射
case3: 沒有name和object
這種情況直接把obs
對象存放在了Observation *wildcard
鏈表結(jié)構(gòu)中
接口2
源碼
接口功能: 此接口實現(xiàn)的功能是在接收到通知時,在指定隊列queue
執(zhí)行block
// 這個api使用頻率較低茧彤,怎么實現(xiàn)在指定隊列回調(diào)block的骡显,值得研究
- (id) addObserverForName: (NSString *)name
object: (id)object
queue: (NSOperationQueue *)queue
usingBlock: (GSNotificationBlock)block
{
// 創(chuàng)建一個臨時觀察者
GSNotificationObserver *observer =
[[GSNotificationObserver alloc] initWithQueue: queue block: block];
// 調(diào)用了接口1的注冊方法
[self addObserver: observer
selector: @selector(didReceiveNotification:)
name: name
object: object];
return observer;
}
復(fù)制代碼
邏輯說明
這個接口依賴于接口1
,只是多了一層代理觀察者GSNotificationObserver
,在關(guān)鍵類結(jié)構(gòu)中已經(jīng)介紹了它惫谤,設(shè)計思路值得學(xué)習(xí)
- 創(chuàng)建一個
GSNotificationObserver
類型的對象observer
壁顶,并把queue
和block
保存下來 - 調(diào)用接口1進行通知的注冊
- 接收到通知時會響應(yīng)
observer
的didReceiveNotification:
方法,然后在didReceiveNotification:
中把block
拋給指定的queue
去執(zhí)行
小結(jié)
- 從上述介紹可以總結(jié)溜歪,存儲是以
name
和object
為維度的若专,即判定是不是同一個通知要從name
和object
區(qū)分,如果他們都相同則認(rèn)為是同一個通知蝴猪,后面包括查找邏輯调衰、刪除邏輯都是以這兩個為維度的,問題列表中的第九題也迎刃而解了 - 理解數(shù)據(jù)結(jié)構(gòu)的設(shè)計是整個通知機制的核心自阱,其他功能只是在此基礎(chǔ)上擴展了一些邏輯
- 存儲過程并沒有做去重操作嚎莉,這也解釋了為什么同一個通知注冊多次則響應(yīng)多次
發(fā)送通知
源碼
發(fā)送通知的核心邏輯比較簡單,基本上就是查找和調(diào)用響應(yīng)方法沛豌,核心函數(shù)如下
// 發(fā)送通知
- (void) postNotificationName: (NSString*)name
object: (id)object
userInfo: (NSDictionary*)info
{
// 構(gòu)造一個GSNotification對象趋箩, GSNotification繼承了NSNotification
GSNotification *notification;
notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
notification->_name = [name copyWithZone: [self zone]];
notification->_object = [object retain];
notification->_info = [info retain];
// 進行發(fā)送操作
[self _postAndRelease: notification];
}
//發(fā)送通知的核心函數(shù),主要做了三件事:查找通知加派、發(fā)送叫确、釋放資源
- (void) _postAndRelease: (NSNotification*)notification {
//step1: 從named、nameless芍锦、wildcard表中查找對應(yīng)的通知
...
//step2:執(zhí)行發(fā)送启妹,即調(diào)用performSelector執(zhí)行響應(yīng)方法,從這里可以看出是同步的
[o->observer performSelector: o->selector
withObject: notification];
//step3: 釋放資源
RELEASE(notification);
}
復(fù)制代碼
邏輯說明
其實上述代碼注釋說的很清晰了醉旦,主要做了三件事
- 通過
name & object
查找到所有的obs
對象(保存了observer
和sel
)饶米,放到數(shù)組中 - 通過
performSelector:
逐一調(diào)用sel
,這是個同步操作 - 釋放
notification
對象
小結(jié)
從源碼邏輯可以看出發(fā)送過程的概述:從三個存儲容器中:named
车胡、nameless
檬输、wildcard
去查找對應(yīng)的obs
對象,然后通過performSelector:
逐一調(diào)用響應(yīng)方法匈棘,這就完成了發(fā)送流程
核心點:
- 同步發(fā)送
- 遍歷所有列表丧慈,即注冊多次通知就會響應(yīng)多次
刪除通知
這里源碼太長而且基本上都是查找刪除邏輯,不一一列舉主卫,感興趣的去下載源碼看下吧
要注意的點:
- 查找時仍然以
name
和object
為維度的逃默,再加上observer
做區(qū)分 - 因為查找時做了這個鏈表的遍歷,所以刪除時會把重復(fù)的通知全都刪除掉
// 刪除已經(jīng)注冊的通知
- (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ā)送,或可稱為延時發(fā)送瘩将,它是利用了runloop
的時機來觸發(fā)的
入隊
下面為精簡版的源碼吟税,看源碼的注釋凹耙,基本上能明白大致邏輯
- 根據(jù)
coalesceMask
參數(shù)判斷是否合并通知 - 接著根據(jù)
postingStyle
參數(shù),判斷通知發(fā)送的時機肠仪,如果不是立即發(fā)送則把通知加入到隊列中:_asapQueue
肖抱、_idleQueue
核心點:
- 隊列是雙向鏈表實現(xiàn)
- 當(dāng)postingStyle值是立即發(fā)送時,調(diào)用的是
NSNotificationCenter
進行發(fā)送的异旧,所以NSNotificationQueue
還是依賴NSNotificationCenter
進行發(fā)送
/*
* 把要發(fā)送的通知添加到隊列意述,等待發(fā)送
* NSPostingStyle 和 coalesceMask在上面的類結(jié)構(gòu)中有介紹
* modes這個就和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進行發(fā)送
[_center postNotification: notification];
break;
}
case NSPostASAP:
// 添加到_asapQueue隊列荤崇,等待發(fā)送
add_to_queue(_asapQueue, notification, modes, _zone);
break;
case NSPostWhenIdle:
// 添加到_idleQueue隊列,等待發(fā)送
add_to_queue(_idleQueue, notification, modes, _zone);
break;
}
}
復(fù)制代碼
發(fā)送通知
這里截取了發(fā)送通知的核心代碼匹涮,這個發(fā)送通知邏輯如下:
-
runloop
觸發(fā)某個時機天试,調(diào)用GSPrivateNotifyASAP()
和GSPrivateNotifyIdle()
方法,這兩個方法最終都調(diào)用了notify()
方法 -
notify()
所做的事情就是調(diào)用NSNotificationCenter
的postNotification:
進行發(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é)
對于NSNotificationQueue
總結(jié)如下
- 依賴
runloop
然低,所以如果在其他子線程使用NSNotificationQueue
喜每,需要開啟runloop - 最終還是通過
NSNotificationCenter
進行發(fā)送通知,所以這個角度講它還是同步的 - 所謂異步雳攘,指的是非實時發(fā)送而是在合適的時機發(fā)送带兜,并沒有開啟異步線程
主線程響應(yīng)通知
異步線程發(fā)送通知則響應(yīng)函數(shù)也是在異步線程,如果執(zhí)行UI刷新相關(guān)的話就會出問題吨灭,那么如何保證在主線程響應(yīng)通知呢刚照?
其實也是比較常見的問題了,基本上解決方式如下幾種:
- 使用
addObserverForName: object: queue: usingBlock
方法注冊通知喧兄,指定在mainqueue
上響應(yīng)block
- 在主線程注冊一個
machPort
无畔,它是用來做線程通信的,當(dāng)在異步線程收到通知吠冤,然后給machPort
發(fā)送消息浑彰,這樣肯定是在主線程處理的,具體用法去網(wǎng)上資料很多拯辙,蘋果官網(wǎng)也有
總結(jié)
本文寫的內(nèi)容比較多郭变,以GNUStep源碼為基礎(chǔ)進行研究,全面闡述了通知的存儲涯保、發(fā)送碗旅、異步發(fā)送等原理曙搬,對研究學(xué)習(xí)有很大幫助
最后推薦個我的高級iOS交流群:761407670 進群密碼000,有一個共同的圈子很重要柿究,結(jié)識人脈僚纷!里面都是iOS開發(fā)梢睛,全棧發(fā)展逛裤,歡迎入駐,共同進步狈蚤!