NSNotification

1、NSNotification

NSNotification包含了一些用于向其他对象发送通知的必要信息,发送通知通过NSNotificationCenter发送,其中NSNotification主要的字段有如下几个,也是发送通知必要的,注意NSNotification是一个不可变的对象。
name:通知的名称,用于通知的唯一标识
object:保存发送通知的对象
userinfo:保存给通知接受者传递的额外信息

2、NSNotificationCenter

NSNotificationCenter提供了一套机制来发送通知,本质上来讲NSNotificationCenter其实就是一个通知派发表。暴露出来的方法也就三种。前两种是对观察者的管理,第三种是用于发送通知。

添加观察者:
addObserver:selector:name:object:
移除通知观察者:
removeObserver:removeObserver:name:object:
发出通知:
postNotification:postNotificationName:object:postNotificationName:object:userInfo: 
这里有下面几点需要说明:
  • 参数object表示的是观察者只会接受来至object对象发出的所注册的通知。而不会接受其他对象发送的所注册的通知。
  • 方法addObserverForName:object:queue:usingBlock:。因为平时这个用得不是特别多。相比addObserver:selector:name:object:这种方式添加通知,多了个queue和block。这里的queue就是决定将block提交到那个队列里面执行。通知接受是和发送通知的线程是同一个。常见的会把这个queue设置为主队列,因为主队列的任务都会在主线程下完成,所以可以用这种方式来实现通知更新UI。而不使用注册SEL的方式。
// 参数queue如果选择nil,也是自动选择主线程
[[NSNotificationCenter defaultCenter] addObserverForName:@"首页按钮" object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull note) {
    self.btn.backgroundColor = UIColor.redColor;
}];
[[NSNotificationCenter defaultCenter] postNotificationName:@"首页按钮" object:nil];

3、NSNotificationQueue

NSNotificationQueue在NSNotificationCenter起到了一个缓冲的作用。尽管NSNotificationCenter已经分发通知,但放入队列的通知可能会延迟,直到当前的runloop结束或runloop处于空闲状态才发送。具体策略是由后面的参数决定。
如果有多个相同的通知,可以在NSNotificationQueue进行合并,这样只会发送一个通知。NSNotificationQueue会通过先进先出的方式来维护NSNotification的实例,当通知实例位于队列首部,通知队列会将它发送到通知中心,然后依次的向注册的所有观察者派发通知。
每个线程有一个默认的和 default notification center相关联的的通知队列。

image.png

如上图所示主要是提供了一些方法给外部调用。通过调用initWithNotificationCenter和外部的NSNotificationCenter关联起来,最终也是通过NSNotificationCenter来管理通知的发送、注册。除此之外这里有两个枚举值需要特别注意一下。

  • NSPostingStyle:用于配置通知什么时候发送
    • NSPostASAP:在当前通知调用或者计时器结束发出通知
    • NSPostWhenIdle:当runloop处于空闲时发出通知
    • NSPostNow:在合并通知完成之后立即发出通知。
  • NSNotificationCoalescing(注意这是一个NS_OPTIONS):用于配置如何合并通知
    • NSNotificationNoCoalescing:不合并通知
    • NSNotificationCoalescingOnName:按照通知名字合并通知
    • NSNotificationCoalescingOnSender:按照传入的object合并通知

4、三者之间的关系

关系.jpg

5、通知的实现原理

NSNotificationCenter是中心管理类,实现较复杂。总的来讲在NSNotificationCenter中定义了两个Table,同时为了封装观察者信息,也定义了一个Observation保存观察者信息。他们的结构体可以简化如下:

typedef struct NCTbl {
  Observation*   wildcard;    /* 保存既没有没有传入通知名字也没有传入object的通知*/
  MapTable       nameless;   /*保存没有传入通知名字的通知 */
  MapTable       named;        /*保存传入了通知名字的通知 */
} NCTable;

typedef struct  Obs {
  id        observer;        /* 保存接受消息的对象*/
  SEL       selector;      /* 保存注册通知时传入的SEL*/
  struct Obs*    next;    /* 保存注册了同一个通知的下一个观察者*/
  struct NCTbl*  link;    /* 保存该Observation的Table*/
} Observation;

在NSNotificationCenter内部一共保存了两张表。一张用于保存添加观察者的时候传入了NotifcationName的情况;一张用于保存添加观察者的时候没有传入了NotifcationName的情况,下面分两种情况分析。

Table

Named Table

先看一下表中保存的内容及Key,Value类型

image.png

在Named Table中,NotifcationName作为表的key,因为我们在注册观察者的时候是可以传入一个参数object用于只监听指定该对象发出的通知,并且一个通知可以添加多个观察者,所以还需要一张表来保存object和Observer的对应关系。这张表的是key、Value分别是以object为Key,Observer为value。如何来实现保存多个观察者的情况呢?用链表这种数据结构最合适不过了。
所以对于Named Table而言,最终的结构:

  • 首先外层有一个Table,以通知名称为Key。其Value同样是一个Table(简称内Table).
  • 为了实现可以传入一个参数object用于只监听指定该对象发出的通知,及一个通知可以添加多个观察者。则内Table得以传入的Object为Key,用链表来保存所有的观察者,并且以这个链表为Value。

在实际开发中我们经常传一个nil的object。这个时候系统会根据nil自动生产一个key(可以理解为一个nil_key)。相当于这个key对应的value(链表)保存的就是对于当前NotifcationName没有传入object的所有观察者。当NotifcationName被发送时,所有在链表中的观察者都会收到通知。

UnNamed Table

UnNamed Table结构比Named Table简单很多。因为没有NotifcationName作为Key。这里直接就以object为key。比Named Table少了一层Table嵌套。

image.png

如果在注册观察者的时候既没有NotifcationName,同时没有传入Object。经过代码实践,所有的系统通知都会发送到注册的对象这里。恰恰对应到上面提到的数据结构中的wildcard字段。

添加观察者的流程

有了上面的基本的结构关系,再来看添加过程就会很简单。总的过程就是按照上面的数据结构添加数据,中间会判断Table及Observation结点是否存在,不存在则创建新的,存在则直接使用。
首先在初始化NSNotificationCenter会创建一个对象,这个对象里面保存了NamedTable、UNnmedTable和一些其他信息。
所有的添加通知操作最后都会调用到addObserver: selector: name: object:。

  1. 首先会根据传入的参数,实例化一个Observation。这个Observation保存了观察者对象、接收到通知观察者对所执行的方法,由于Observation是一个链表,还保存了下一个Observation的地址。
  2. 根据是否传入通知的Name选择在Named Table还是UNamed Table操作。
  3. 如果传入通知的Name,则会先去用Name去查找是否已经有对应的Value(注意这个时候返回的Value是一个Table)
  4. 如果没有对应的Value,则创建一个新的Table,然后将这个Table以Name为Key添加到Named Table。如果有Value,那么直接去取出这个Table。
  5. 得到了保存Observation的Table之后,就通过传入的object去拿对应的链表。如果object为空,会默认有一个key表示传入object为空的情况,取的时候也会直接用这个key去取。表示所有任何地方发送通知都会监听。
  6. 如果在保存Observation的Table中根据object作为key没有找到对应的链表,则会创建一个节点,作为头结点插入进去;如果找到了则直接在链表末尾插入之前实例化好的Observation。

在没有传入通知名字的情况和上面的过程类似,只不过是直接根据object去对应的链表而已。
如果既没有传入NotifcationName也没有传入Object。则这个观察者会添加到wildcard(在介绍Table数据结构中提到的)链表中。

发送通知的流程

发送通知的一般是调用postNotificationName:(NSNotificationName)aName object:(nullable id)anObject来实现。
postNotificationName内部会实例化一个NSNotification来保存传入的各种参数。根据之前介绍的数据结构,包含name、object和一个userinfo。
发送通知的流程总体来讲就是根据NotifcationName查找到对应的Observer链表,然后遍历整个链表,给每个Observer结点中的对象发送信息(也即是调用对象的SEL方法)

  1. 首先会定义一个数组ObserversArray来保存需要通知的Observer。之前在添加观察者的时候把既没有传入NotifcationName也没有传入object保存在了wildcard。因为这样的观察者会监听所有NotifcationName的通知,所以先把wildcard链表遍历一遍,将其中的Observer加到数组中ObserversArray
  2. 找到以object为key的Observer链表。这个过程分为在Named Table中找,以及在UNamed Table中查找。然后将遍历找到的链表,同样加入到最开始创建的数组ObserversArray中。
  3. 至此所有关于NotifcationName的Observer(wildcard+UNamed Table+Named Table)已经加入到了数组ObserversArray。接下来就是遍历这个ObserversArray数组,一次取出其中的Observer结点。因为这个节点保存了观察者对象以及selector。所以最终调用形式如下:
    [observerNode->observer performSelector: o->selector withObject: notification];

这个方式也就能说明,发送通知的线程和接收通知的线程是同一个线程。在工作中经常为了保持在主线程中更新UI,所以经常会做接受通知的方法中用dispatch_async(dispatch_get_main_queue(), ^{});处理一下,以保障无论从什么线程发出的通知,都能在主线程中更新UI。

6、小结

存储(添加通知):
  1. 存储是以name和object为维度的,即判定是不是同一个通知要从name和object区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都是以这两个为维度的。
  2. 理解数据结构的设计是整个通知机制的核心,其他功能只是在此基础上扩展了一些逻辑
  3. 存储过程并没有做去重操作,这也解释了为什么同一个通知注册多次则响应多次
发送通知:
  1. 通过name&bject查找到所有的obs对象(保存了observer和sel),放到数组中
  2. 通过performSelector:逐一调用sel,这是个同步操作
  3. 释放notification对象
    从源码逻辑可以看出发送过程的概述:从三个存储容器中:named、nameless、wildcard去查找对应的obs对象,然后通过performSelector:逐一调用响应方法,这就完成了发送流程

核心点:

  1. 同步发送
  2. 遍历所有列表,即注册多次通知就会响应多次
删除通知:
  1. 查找时仍然以name和object为维度的,再加上observer做区分
  2. 因为查找时做了这个链表的遍历,所以删除时会把重复的通知全都删除掉
异步通知

上面介绍的NSNotificationCenter都是同步发送的,而这里介绍关于NSNotificationQueue的异步发送,从线程的角度看并不是真正的异步发送,或可称为延时发送,它是利用了runloop的时机来触发的

入队:
  1. 根据coalesceMask参数判断是否合并通知
  2. 接着根据postingStyle参数,判断通知发送的时机,如果不是立即发送则把通知加入到队列中:_asapQueue、_idleQueue

核心点:

  1. 队列是双向链表实现
  2. 当postingStyle值是立即发送时,调用的是NSNotificationCenter进行发送的,所以NSNotificationQueue还是依赖NSNotificationCenter进行发送
发送通知
  1. runloop触发某个时机,调用GSPrivateNotifyASAP()和GSPrivateNotifyIdle()方法,这两个方法最终都调用了notify()方法
  2. notify()所做的事情就是调用NSNotificationCenter的postNotification:进行发送通知
对于NSNotificationQueue总结如下
  1. 依赖runloop,所以如果在其他子线程使用NSNotificationQueue,需要开启runloop
  2. 最终还是通过NSNotificationCenter进行发送通知,所以这个角度讲它还是同步的
  3. 所谓异步,指的是非实时发送而是在合适的时机发送,并没有开启异步线程
  4. 如果发送的消息不在主线程,接受消息就会自动在子线程中执行(不管你在主线程或者是子线程中注册消息)。那怎么办?在你收到消息通知的时候,注意选择你要执行的线程。如果在子线程中发送消息,则需要重定向到主线程中执行任务(比如刷新UI)。使用方式如下:
-(id)addObserverForName:(NSString*)nameobject:(id)objqueue:(NSOperationQueue*)queue

你可能感兴趣的:(NSNotification)