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

推荐阅读更多精彩内容