深入理解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非常非常大的时候如何保证查找的效率,而且这种场景在实际开发中也经常遇到,尤其是一些大型项目,随随便便就是成百上千个通知。关于这个问题,后面分析吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容