通知概念

苹果官方文档有一段对通知的介绍如下:

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 非常大的时候是如何保证查询效率的。感兴趣的同学可以进行更深层次的研究。

参考文献

本文由 简悦 SimpRead 转码
Notification
NSNotificationCenter
NSNotificationQueue
Delivering Notifications To Particular Threads
Gunstep NSNotififcationCenter sourcecode