IOS基礎原理:Notifications

原創:知識點總結性文章
創作不易,請珍惜,之后會持續更新,不斷完善
個人比較喜歡做筆記和寫總結,畢竟好記性不如爛筆頭哈哈,這些文章記錄了我的IOS成長歷程,希望能與大家一起進步
溫馨提示:由于簡書不支持目錄跳轉,大家可通過command + F 輸入目錄標題后迅速尋找到你所需要的內容

目錄

  • 一、使用
    • 1、提供的屬性和方法
    • 2、隊列的合并策略和發送時機
    • 3、注意點
  • 二、注冊通知源碼解析
    • 1、存儲容器
    • 2、解析注冊通知方法
    • 3、判斷是否是同一個通知的3種情況
  • 三、發送通知與刪除通知源碼解析
    • 1、發送通知源碼解析
    • 2、刪除通知源碼解析
  • 四、異步通知
    • 1、NSNotificationQueue的異步發送
    • 2、把要發送的通知添加到隊列,等待發送
    • 3、發送通知
    • 4、主線程響應通知
  • Demo
  • 參考文獻

一、使用

1、提供的屬性和方法

NSNotification
- (NSString*) name; // 通知的name
- (id) object; // 攜帶的對象
- (NSDictionary*) userInfo; // 配置信息
NSNotificationCenter
// 添加通知
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

// 發送通知
- (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;
NSNotificationQueue
// 把通知添加到隊列中,NSPostingStyle是個枚舉
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;

// 刪除通知,把滿足合并條件的通知從隊列中刪除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;

用于異步發送消息的通知隊列,這個異步并不是開啟線程,而是把通知存到雙向鏈表實現的隊列里面,等待某個時機觸發。觸發時調用NSNotificationCenter的發送接口進行發送通知,這么看NSNotificationQueue最終還是調用NSNotificationCenter進行消息的分發,另外NSNotificationQueue是依賴runloop的,所以如果線程的runloop未開啟則無效。


2、隊列的合并策略和發送時機

把通知添加到隊列等待發送,同時提供了一些附加條件供開發者選擇,如:什么時候發送通知、如何合并通知等,系統給了如下定義。

表示通知的發送時機
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空閑時發送通知
    NSPostASAP = 2, // 盡快發送,這種情況稍微復雜,這種時機是穿插在每次事件完成期間來做的
    NSPostNow = 3 // 立刻發送或者合并通知完成之后發送
};
通知合并的策略,有些時候同名通知只想存在一個,這時候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0, // 默認不合并
    NSNotificationCoalescingOnName = 1, // 只要name相同,就認為是相同通知
    NSNotificationCoalescingOnSender = 2  // object相同
};

3、注意點

頁面銷毀時不移除通知會崩潰嗎

可以,因為notificationcenter對觀察者的引用是weak,當觀察者釋放的時候,觀察者的指針值被置為nil

多次添加同一個通知會是什么結果?多次移除通知呢

會調用多次observeraction。多次移除沒有任何影響。


二、注冊通知源碼解析

1、存儲容器

NCTable是根容器,由NSNotificationCenter持有

NCTable結構體中核心的三個變量:wildcardnamednameless,在源碼中直接用宏定義表示了:WILDCARDNAMELESSNAMED

typedef struct NCTbl
{
    // 鏈表結構,保存既沒有name也沒有object的通知
    Observation *wildcard;
    
    // 存儲沒有name但是有object的通知
    GSIMapTable nameless;
    
    // 存儲帶有name的通知,不管有沒有object
    GSIMapTable named;
    ...
} NCTable;
Observation是存儲觀察者和響應方法的結構體
typedef    struct    Obs
{
    id        observer;// 觀察者,接收通知的對象
    SEL        selector;// 響應方法
    struct Obs    *next;// 鏈表中的下一個Observation
    ...
} Observation;

2、解析注冊通知方法

  • 判定是不是同一個通知要從nameobject區分,如果他們都相同則認為是同一個通知,后面包括查找邏輯、刪除邏輯都以此為基礎。
  • 存儲過程并沒有做去重操作,這也解釋了為什么同一個通知注冊多次則響應多次
a、提供給外界使用的注冊通知方法
  • observer:觀察者,即通知的接收者
  • selector:接收到通知時的響應方法
  • name:通知name
  • object:攜帶對象
- (void) addObserver: (id)observer selector: (SEL)selector name: (NSString*)name object: (id)object
{
    // 前置條件判斷
    ...
    
    // 創建一個observation對象,持有觀察者和SEL,下面進行的所有邏輯就是為了存儲它
    o = obsNew(TABLE, selector, observer);
    
    ...
}

b、情況一:如果name存在
if (name) {...}

? NAMED是個宏,表示名為named的字典。如果通知的name存在,則以namekeynamed字典中取出值n(這個n其實被MapNode包裝了一層,便于理解這里直接認為沒有包裝),這個n還是個字典。

n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);

? n不存在,則先取緩存,如果緩存沒有則新建一個map

if (n == 0)
{
    m = mapNew(TABLE);
    GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
    ...
}

? n存在則把值取出來賦值給m

else
{
    m = (GSIMapTable)n->value.ptr;
}

? 然后以objectkey,從字典中取出對應的值,這個值就是Observation類型的鏈表,然后把剛開始創建的Observation對象o存儲進去。

n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n == 0)
{// 不存在,則創建
    o->next = ENDOBS;
    GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
}
else
{
    list = (Observation*)n->value.ptr;
    o->next = list->next;
    list->next = o;
}

c、情況二:如果name為空,但object不為空
else if (object)
{
    ...
}

? 以objectkey,從nameless字典中取出對應的valuevalue是個鏈表結構。

n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);

? 不存在則新建鏈表,并存到map

if (n == 0)
{
    o->next = ENDOBS;
    GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}

? 存在則把值接到鏈表的節點上

else
{ 
    ...
}

d、情況三:name 和 object 都為空則存儲到wildcard鏈表中
else
{
    o->next = WILDCARD;
    WILDCARD = o;
}

3、判斷是否是同一個通知的3種情況

情況一:存在name(無論object是否存在)

如果注冊通知時傳入name,那么會是一個雙層的存儲結構。首先找到NCTable中的named表,這個表存儲了name的通知。接著以name作為key,找到value,這個value依然是一個map。最后,map的結構是以object作為keyObservation對象為value,這個Observation對象的結構上面已經解釋,主要存儲了observer & SEL

情況二:只存在object

objectkey,從nameless字典中取出value,此value是個Observation類型的鏈表。接著把創建的Observation類型的對象o存儲到鏈表中。只存在object時存儲只有一層,那就是objectObservation對象之間的映射。

情況三:沒有name和object

這種情況直接把Observation對象存放在了Observation *wildcard 鏈表結構中。


三、發送通知與刪除通知源碼解析

1、發送通知源碼解析

發送通知的核心邏輯比較簡單,基本上就是查找和調用響應方法,從三個存儲容器中:namednamelesswildcard去查找對應的Observation對象,然后通過performSelector:逐一調用響應方法,這就完成了發送流程。

a、解析發送通知方法
- (void)postNotificationName: (NSString*)name object: (id)object userInfo: (NSDictionary*)info
{
    ...
}
? 構造一個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];
? 進行發送操作
[self _postAndRelease: notification];

b、發送通知的核心函數

主要做了三件事:查找通知、發送、釋放資源。

- (void)_postAndRelease: (NSNotification*)notification
{
    ...
}

? 通過name & objectnamednamelesswildcard表中查找對應的通知(保存了observersel)。

...

? 執行發送,即調用performSelector執行響應方法,從這里可以看出是同步的。

[o->observer performSelector: o->selector
                      withObject: notification];

? 釋放notification對象。

RELEASE(notification);

2、刪除通知源碼解析

因為查找時做了這個鏈表的遍歷,所以刪除時會把重復的通知全都刪除掉

- (void)removeObserver: (id)observer
{
    if (observer == nil) return;
    
  [self removeObserver: observer name: nil object: nil];
}

查找時仍然以nameobject為準,再加上observer做區分。

- (void)removeObserver: (id)observer name: (NSString*)name object: (id)object
{
    if (name == nil && object == nil && observer == nil)
        return;
    ...
}

四、異步通知

1、NSNotificationQueue的異步發送

上面介紹的NSNotificationCenter都是同步發送的,接受消息和發送消息是在一個線程里。這里介紹關于NSNotificationQueue的異步發送,通過NSNotificationQueue將通知添加到隊列當中,立即將控制權返回給調用者,在合適的時機發送通知,從而不會阻塞當前的調用。從線程的角度看并不是真正的異步發送,或可稱為延時發送,它是利用了runloop的時機來觸發的,所以如果在其他子線程使用NSNotificationQueue,需要開啟runloop。由于最終還是通過NSNotificationCenter進行發送通知,所以從這個角度講它還是同步的。所謂異步,指的是非實時發送而是在合適的時機發送,并沒有開啟異步線程。


2、把要發送的通知添加到隊列,等待發送

NSPostingStylecoalesceMask在上面的類結構中有介紹。modes這個就和runloop有關了,指的是runloopmode

- (void) enqueueNotification: (NSNotification*)notification
        postingStyle: (NSPostingStyle)postingStyle
        coalesceMask: (NSUInteger)coalesceMask
            forModes: (NSArray*)modes
{
    ...
}

? 根據coalesceMask參數判斷是否合并通知

if (coalesceMask != NSNotificationNoCoalescing)
{
    [self dequeueNotificationsMatching: notification
                          coalesceMask: coalesceMask];
}

? 接著根據postingStyle參數,判斷通知發送的時機

switch (postingStyle)
{
    ...
}

? runloop立即回調通知方法,同步發送

case NSPostNow:
{
    // 如果是立馬發送,則調用NSNotificationCenter進行發送
    [_center postNotification: notification];
}

? runloop在執行timer事件或sources事件的時候回調通知方法,異步發送

case NSPostASAP:
    // 添加到_asapQueue隊列,等待發送
    add_to_queue(_asapQueue, notification, modes, _zone);

? runloop空閑的時候回調通知方法,異步發送

case NSPostWhenIdle:
    // 添加到_idleQueue隊列,等待發送
    add_to_queue(_idleQueue, notification, modes, _zone);

3、發送通知

runloop觸發某個時機,調用GSPrivateNotifyASAP()GSPrivateNotifyIdle()方法,這兩個方法最終都調用了notify()方法。notify()所做的事情就是調用NSNotificationCenterpostNotification:進行發送通知。

a、發送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_asapQueue,
        mode,
        item->queue->_zone);
}
b、發送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_idleQueue,
        mode,
        item->queue->_zone);
}
c、循環遍歷發送通知
static void notify(NSNotificationCenter *center,
                   NSNotificationQueueList *list,
                   NSString *mode, NSZone *zone)
{
    for (pos = 0; pos < len; pos++)
    {
      NSNotification *n = (NSNotification*)ptr[pos];

      [center postNotification: n];
      RELEASE(n);
    }
}

4、主線程響應通知

異步線程發送通知則響應函數也是在異步線程,如果執行UI刷新相關的話就會出現問題,那么如何保證在主線程響應通知呢?可以使用addObserverForName: object: queue: usingBlock方法注冊通知,指定在mainqueue上響應block


Demo

Demo在我的Github上,歡迎下載。
BasicsDemo

參考文獻

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容