底层初窥——NSNotificationCenter
介绍
消息通知在项目中使用是很频繁的,但是我们平常之事关注于使用,这篇文章我们稍微深入了解一下消息中心是怎么运作的。
通知机制的核心是一个与线程关联的单例对象叫通知中心(NSNotificationCenter)。通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。
苹果并没有开源相关代码,但是可以读下GNUStep的源码,基本上实现方式很具有参考性。
数据结构
这里我们的数据结构及流程分析,都是基于 GNUStep的源码。
NSNotificationCenter:消息中心
1 | typedef struct NCTbl { |
- wildcard
- wildcard是链表的数据结构,如果在注册观察者时既没有传入NotificationName,也没有传入object,就会添加到wildcard的链表中。注册到这里的观察者能接收到 所有的系统通知。
- nameless
- 添加观察者时没有传入 NoficationName 的表
- named
- 添加观察者时传入了 NotificationName 的表
named
在 named 表中,NotifcationName 作为表的 key,因为我们在注册观察者的时候是可以传入一个参数 object 用于只监听指定该对象发出的通知,并且一个通知可以添加多个观察者,所以还需要一张表来保存 object 和 Observer 的对应关系。这张表的是 key、Value 分别是以 object 为 Key,Observer 为 value。用了链表这种数据结构实现保存多个观察者的情况。
在实际开发过程中 object 参数我们经常传 nil,这时候系统会根据 nil 自动生成一个 key,相当于这个 key 对应的 value(链表)保存的就是当前通知传入了 NotificationName 没有传入 object 的所有观察者。
nameless
nameless 表,较 named 表就简单了许多。因为少了 NSNotificationName 作为 Key 值,所以少了一层嵌套。
wildcard
在注册观察者时既没有传入 NSNotificationName,也没有传入 object,就会添加到 wildcard 的链表中。注册到这里的观察者能接收到所有的系统通知。
Observation:保存了观察者信息
1 | typedef struct Obs { |
工作流程
添加观察者流程
- 首先会根据传入的参数实例化一个 Observation,Observation 对象保存了观察者对象,接收到通知观察者所执行的方法,以及下一个 Observation 对象的地址。
- 根据是否传入 NotificationName 选择操作 Named Table 还是 Nameless Table。
- 若传入了 NotificationName,则会以 NotificationName 为 key 去查找对应的 Value,若找到 value,则取出对应的 value;若未找到对应的 value,则新建一个 table,然后将这个 table 以 NotificationName 为 key 添加到 Named Table 中。
- 若在保存 Observation 的 table 中,以 object 为 key 取对应的链表。若找到了则直接在链接末尾插入之前实例化好的 Observation;若未找到则以之前实例化好的 Observation 对象作为头节点插入进去。
发送通知流程
- 首先会创建一个数组 observerArray 用来保存需要通知的 observer。
- 遍历 wildcard 链表,将 observer 添加到 observerArray 数组中。
- 若存在 object,在 nameless table 中找到以 object 为 key 的链表,然后遍历找到的链表,将 observer 添加到 observerArray 数组中。
- 若存在 NotificationName,在 named table 中以 NotificationName 为 key 找到对应的 table,然后再在找到的 table 中以 object 为 key 找到对应的链表,遍历链表,将 observer 添加到 observerArray 数组中。如果 object 不 为nil,则以 nil 为 key 找到对应的链表,遍历链表,将 observer 添加到 observerArray 数组中。
- 至此所有关于当前通知的 observer(wildcard+nameless+named)都已经加入到了数组 observerArray 中。遍历 observerArray 数组,取出其中 的observer 节点(包含了观察者对象和 selector),调用形式如下:
1
[o->observer performSelector: o->selector withObject: notification];
移除通知流程
- 若 NotificationName 和 object 都为 nil,则清空 wildcard 链表。
- 若 NotificationName 为 nil,遍历 named table,若 object 为 nil,则清空 named table,若 object 不为 nil,则以 object 为 key 找到对应的链表,然后清空链表。在 nameless table 中以 object 为 key 找到对应的 observer 链表,然后清空,若 object 也为 nil,则清空 nameless table。
- 若 NotificationName 不为nil,在 named table 中以 NotificationName 为 key 找到对应的 table,若 object 为 nil,则清空找到的 table,若 object 不为 nil,则以 object 为 key 在找到的 table 中取出对应的链表,然后清空链表。
一些问题
这一节收集了几个常见的 NSNotificationCenter 相关的问题。
通知的发送时同步的,还是异步的?发送消息与接收消息的线程是同一个线程么?
通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。
在抛出通知以后,观察者在通知事件处理完成以后(可以通过休眠3秒来测试),抛出者才会往下继续执行,也就是说这个过程默认是同步的;当发送通知时,通知中心会一直等待所有的 observer 都收到并且处理了通知才会返回到 poster。
接收通知的线程,和发送通知所处的线程是同一个线程。也就是说如果如果要在接收通知的时候更新 UI,需要注意发送通知的线程是否为主线程。
1 | - (void)viewDidLoad { |
如何异步发送消息?
- 让通知事件处理方法在子线程中执行.
1 | - (void)viewDidLoad { |
- 可以通过 NSNotificationQueue 的 enqueueNotification: postingStyle: 和 enqueueNotification: postingStyle: coalesceMask: forModes: 方法将通告放入队列,实现异步发送,在把通告放入队列之后,这些方法会立即将控制权返回给调用对象。
1 | - (void)viewDidLoad { |
NSNotificationQueue 和 runloop 的关系?
postringStyle 参数就是定义通知调用和 runloop 状态之间关系。
该参数的三个可选参数:
- NSPostWhenIdle:通知回调方法是等待到当下线程 runloop 进入等待状态才会调用。
- NSPostASAP:通知回调方法是等待到当下线程 runloop 开始接收事件源的时候就会调用。
- NSPostNow:其实和直接用默认的通知中心添加通知是一样的,通知马上调用回调方法。
如何保证通知接收的线程在主线程?
页面销毁时不移除通知会崩溃吗?
在观察者对象释放之前,需要调用 ==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: 方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。
1 | - (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)); |
多次添加同一个通知会是什么结果?多次移除通知呢?
多次添加同一个通知,会导致观察者方法被执行多次。
多次移除通知,没有关系。
移除通知
1 | // 添加 |
下面的方式能接收到通知吗?为什么
1 | // 添加观察 |
不会,上文介绍 NSNotificationCenter 时介绍了 center 的结构。
- 注册通知在添加observer时,路径为 TestNotification -> @1 -> self
- 发送通知在查找observer时,路径为 TestNotification -> nil -> observer list