iOS NSNotification實現原理

通知概念

蘋果官方文檔有一段對通知的介紹如下:

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)異步發送通知。


NotificationCenter

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初始化,否則會引起如下崩潰:

notification init error

但是一般情況下不會直接這樣創建通知對象。實際開發中更多的是直接調用NSNoficationCenterpostNotificationName: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

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嵌套。

NamelessTable

wildcard
wildcard是鏈表的數據結構,如果在注冊觀察者時既沒有傳入NotificationName,也沒有傳入object,就會添加到wildcard的鏈表中。注冊到這里的觀察者能接收到 所有的系統通知。

添加觀察者流程
有了上面基本的結構關系,再來看添加過程就會很簡單。在初始化NotificationCenter時會創建一個對象,這個對象里保存了Named Table、Nameless Table、wildcard和一些其它信息。所有注冊觀察者的操作最后都會調用addObserver:selector:name:object:

  1. 首先會根據傳入的參數實例化一個Observation,Observation對象保存了觀察者對象,接收到通知觀察者所執行的方法,以及下一個Observation對象的地址。
  2. 根據是否傳入NotificationName選擇操作Named Table還是Nameless Table。
  3. 若傳入了NotificationName,則會以NotificationName為key去查找對應的Value,若找到value,則取出對應的value;若未找到對應的value,則新建一個table,然后將這個table以NotificationName為key添加到Named Table中。
  4. 若在保存Observation的table中,以object為key取對應的鏈表。若找到了則直接在鏈接末尾插入之前實例化好的Observation;若未找到則以之前實例化好的Observation對象作為頭節點插入進去。

沒有傳入NotificationName的情況和上面的過程類似,只不過是直接根據對應的object為key去找對應的鏈表而已。如果既沒有傳入NotificationName也沒有傳入object,則這個觀察者會添加到wildcard鏈表中。

發送通知流程
發送通知一般調用postNotificationName:object:userInfo:來實現,內部會根據傳入的參數實例化一個NSNotification對象,包含name、object、userinfo等信息。

發送通知的流程總體來說是根據NotificationName和object找到對應的鏈表,然后遍歷整個鏈表,給每個Observation節點中保存的oberver發送對應的SEL消息。

  1. 首先會創建一個數組observerArray用來保存需要通知的observer。
  2. 遍歷wildcard鏈表,將observer添加到observerArray數組中。
  3. 若存在object,在nameless table中找到以object為key的鏈表,然后遍歷找到的鏈表,將observer添加到observerArray數組中。
  4. 若存在NotificationName,在named table中以NotificationName為key找到對應的table,然后再在找到的table中以object為key找到對應的鏈表,遍歷鏈表,將observer添加到observerArray數組中。如果object不為nil,則以nil為key找到對應的鏈表,遍歷鏈表,將observer添加到observerArray數組中。
  5. 至此所有關于當前通知的observer(wildcard+nameless+named)都已經加入到了數組observerArray中。遍歷observerArray數組,取出其中的observer節點(包含了觀察者對象和selector),調用形式如下:
[o->observer performSelector: o->selector withObject: notification];

這種處理通知的方式也就能說明,發送通知的線程和接收通知的線程是同一線程

移除通知流程
根據前面分析的添加觀察者的流程與發送通知的流程可以類比出移除通知的流程。

  1. 若NotificationName和object都為nil,則清空wildcard鏈表。
  2. 若NotificationName為nil,遍歷named table,若object為nil,則清空named table,若object不為nil,則以object為key找到對應的鏈表,然后清空鏈表。在nameless table中以object為key找到對應的observer鏈表,然后清空,若object也為nil,則清空nameless table。
  3. 若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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念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

推薦閱讀更多精彩內容