深入理解iOS NSNotification

NSNotification在平时开发中使用非常频繁。网上关于NSNotification介绍大多是停留在用法的介绍,基本上没有深入介绍NSNotification原理的文章。故有此文!

通知基础

NSNotification 是iOS中一个调度消息通知的类,采用单例模式设计,在程序中实现传值、回调等地方应用很广。在iOS中,NSNotification是使用观察者模式来实现的用于跨层传递消息。往往也用NSNotification来实现解耦的目的。

通知这种传值方式一般用于一对多的情况,iOS中常见的还有代理传值、block传值等。代理实现和block一般用于一对一的情况。至于具体区别接不赘述了。

总结一下笔者在项目中使用通知的过程中,平时注意的几点:

  • 通知的定义最好统一放在一个头文件中定义好,命名也尽量规范,比如用APP名模块名通知名这种方式,便于区分该通知具体实现什么目的。
  • 全局最好维护一个单例来进行通知的发送。并且建立一张通知发送对象的表及接收通知对象表。因为在比较大的项目中,通知使用很频繁的情况下,很难找到对应的位置。往往给开发埋下了严重的坑。
  • 接收通知的线程,和发送通知所处的线程是同一个线程。也就是说如果如果要在接收通知的时候更新UI,需要注意发送通知的线程是否为主线程。

通知中的数据结构

在介绍原理之前,先弄清通知中的常见数据结构有助于深刻的理解整个过程。这也是笔者分析源码常用方式。

NSNotification

NSNotification包含了一些用于向其他对象发送通知的必要信息,发送通知通过NSNotificationCenter发送,其中NSNotification主要的字段有如下几个,也是发送通知必要的,注意NSNotification是一个不可变的对象

字段名 含义
name 通知的名称,用于通知的唯一标识
object 保存发送通知的对象
userinfo 保存给通知接受者传递的额外信息

可以使用notificationWithName:object:或者notificationWithName:object:userInfo:创建通知对象,但是一般情况下不会这样直接创建。实际工作中更多是直接使用NSNotificationCenter调用postNotificationName:object:或者 postNotificationName:object:userInfo:方法发出通知,这两个方法会在内部直接创建这个对象

NSNotification是一个类簇,不能通过init实例化,比如NSNotification *notif = [[NSNotification alloc]init];这样会引起下面的异常。

但是可以通过装饰构造方法创建实例对象,装饰构造方法如下。:

initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;

如果想要附加更多信息在NSNotification中,可以子类化NSNotification,额外新加的字段。需要注意的一点就是虽然可以自己去实现装饰构造方法,但是切记在自定义的装饰构造方法中不要调用[super init]。

NSNotificationCenter

NSNotificationCenter提供了一套机制来发送通知,本质上来讲NSNotificationCenter其实就是一个通知派发表。至于为什么这么讲,后面在介绍发送流程的时候会详细介绍。

NSNotificationCenter暴露给外部的字段不多就只有一个defaultCenter,而且这个字段还是只读的,其余的全是对通知操作的函数。

暴露出来的方法也就三种。前两种是对观察者的管理,第三种是用于发送通知。

作用 相关方法
添加通知观察者 addObserverForName:object:queue:usingBlock:
addObserver:selector:name:object:
移除通知观察者 removeObserver:
emoveObserver: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的方式。

如果你还不清楚队列与线程的区别,建议自己亲手去实践一遍。可以简单说下主队列,主队列(串行队列)中的任务都是在主线程中完成,无论是同步还是异步执行。

NSNotificationQueue

NSNotificationQueue在NSNotificationCenter起到了一个缓冲的作用。尽管NSNotificationCenter已经分发通知,但放入队列的通知可能会延迟,直到当前的runloop结束或runloop处于空闲状态才发送。具体策略是由后面的参数决定。

如果有多个相同的通知,可以在NSNotificationQueue进行合并,这样只会发送一个通知。NSNotificationQueue会通过先进先出的方式来维护NSNotification的实例,当通知实例位于队列首部,通知队列会将它发送到通知中心,然后依次的像注册的所有观察者派发通知。

每个线程有一个默认和 default notification center相关联的的通知队列。

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

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

通知的实现原理

上面介绍完了关键对象的数据结构,可以用下图归纳总结:


前面提到过NSNotification是一个类簇不能够实例化的,当我们调用initWithName:object: userInfo:方法的时候,系统内部会自己实现一个基于NSNotification的子类NSConcreteNotification。并且在这个子类中重写了NSNotification定义的相关字段及方法。

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类型


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

如果在注册观察者的时候既没有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,来像对象发送信息(也即是调用对象的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。

移除通知的流程

根据前面分析的添加观察及发送通知的流程可以类比出移除通知的流程是如何的。掌握好核心就是操作两个Table及一个链表。

结合上面讲的相关数据结构,移除的通知的流程留给读者自己去思考。

总结

其实分析NSNotification过程中间还有一些细节没有考虑到。比如在整个Table非常非常大的时候如何保证查找的效率,而且这种场景在实际开发中也经常遇到,尤其是一些大型项目,随随便便就是成百上千个通知。关于这个问题,后面分析吧。

推荐阅读更多精彩内容

  • 借却桃花三两片, 且教众子把诗传。 桃花千古得绝境, 陶令孤篇谱世间。 竟用二桃杀壮士, 岂能半脑对纶冠。 规箴世...
    l慕狂书客阅读 696评论 0 2
  • 就是这个身影,这翘臀,这圈腿,一如既往陪伴,走在前面,亦或者迎面跑来...任凭我撒娇撒野,欢笑哭泣,爆粗装逼,他自...
    断念凝时阅读 71评论 0 0
  • Zendaya和Kate Winslet等明星都公开反对修图,好莱坞正在经历一次大变,那就是表现真实的明星。 大家...
    Seed精选阅读 286评论 0 0