NSNotification相关
- 实现原理(结构设计、通知如何存储的、name&observer&SEL之间的关系等)
2. 通知的发送时同步的,还是异步的
同步
- NSNotificationCenter接受消息和发送消息是在一个线程里吗?如何发送异步消息?
通知的接收和发送是在一个线程
里
实际上发送通知都是同步的,不存在异步操作。而所谓的异步发送,也就是延迟发送
,在合适的实际发送。
实现异步发送
:
- 让通知的执行方法异步执行即可
- 通过
NSNotificationQueue
,将通知添加到队列当中,立即将控制权返回给调用者,在合适的时机发送通知,从而不会阻塞当前的调用
参考这篇文章
- NSNotificationQueue是异步还是同步发送?在哪个线程响应
NSPostingStyle的值为:
-
NSPostWhenIdle
和NSPostASAP
:异步发送 -
NSPostNow
:同步发送
响应线程:
默认情况是在主线程中响应的,倘若在调用enqueueNotification将通知添加到队列中时,是在子线程中完成的,那么,响应也会在这个子线程中。
- NSNotificationQueue和runloop的关系
NSNotificationQueue
将通知添加到队列中时,其中postringStyle
参数就是定义通知调用和runloop状态之间关系。
该参数的三个可选参数:
-
NSPostWhenIdle
:runloop空闲的时候回调通知方法 -
NSPostASAP
:runloop在执行timer事件或sources事件完成的时候回调通知方法 -
NSPostNow
:runloop立即回调通知方法
- 如何保证通知接收的线程在主线程
有以下两种方案
- 使用
addObserverForName: object: queue: usingBlock
方法注册通知,指定在mainqueue
上响应block
- 通过在主线程的runloop中添加
machPort
,设置这个port的delegate
,通过这个Port其他线程可以跟主线程通信,在这个port的代理回调中执行的代码肯定在主线程中运行,所以,在这里调用NSNotificationCenter发送通知即可,参考这篇文章
- 页面销毁时不移除通知会崩溃吗
- iOS9.0之前,会crash,原因:通知中心对观察者的引用是
unsafe_unretained
,导致当观察者释放的时候,观察者的指针值并不为nil,出现野指针。 - iOS9.0之后,不会crash,原因:通知中心对观察者的引用是weak。
-
多次添加同一个通知会是什么结果?
多次添加
同一个通知,会导致发送一次这个通知的时候,响应多次通知回调
。 -
多次移除通知呢
多次移除
通知不会产生crash。 -
下面的方式能接收到通知吗?为什么
// 发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
不能
当添加通知监听的时候,我们传入了name和object,所以,观察者的存储链表是这样的:
named表:key(name):value->key(object):value(Observation)
因此在发送通知的时候,如果只传入name而并没有传入object,是找不到
Observation的,也就不能执行观察者回调
关键类结构
NSNotification
@property (readonly, copy) NSNotificationName name; // 通知名称,通知的唯一标识
@property (nullable, readonly, retain) id object; // 任意对象,通常是通知发送者
@property (nullable, readonly, copy) NSDictionary *userInfo; // 通知的附加信息
NSNotificationCenter
这是个单例类
,负责管理通知的创建和发送,属于最核心的类
了。而NSNotificationCenter类主要负责三件事
- 添加通知
- 发送通知
- 移除通知
// 添加通知观察者
- (void)addObserver:(id)observer selector:(SEL)aSelector
name:(nullable NSNotificationName)aName
object:(nullable id)anObject;
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name
object:(nullable id)obj
queue:(nullable NSOperationQueue *)queue
usingBlock:(void (^)(NSNotification *note))block;
// 发出通知
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
// 移除通知观察者
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
注意:
- 若notificationName为nil,通知中心会通知所有与该通知中object相匹配的监听对象。
- 若anObject为nil,通知中心会通知所有与该通知中notificationName相匹配的监听对象。
- iOS9以后NSNofitifcationCenter无需手动移除观察者
在观察者对象释放之前,需要调用==removeOberver==方法将观察者从通知中心移除,否则程序可能会出现崩溃。但从iOS9开始
,即使不移除观察者对象,程序也不会出现异常。
这是因为在iOS9以后,通知中心持有的观察者由==unsafe_unretained==
引用变为==weak==
引用。即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空
。但是通过addObserverForName:object: queue:usingBlock:
方法注册的观察者需要手动释放
,因为通知中心持有的是它们的强引用
。
NSNotificationQueue
功能介绍
通知队列,用于异步发送消息
,这个异步并不是开启线程
,而是把通知存到双向链表
实现的队列
里面,等待某个时机触发时调用NSNotificationCenter
的发送接口
进行发送通知,这么看NSNotificationQueue最终还是调用NSNotificationCenter进行消息的分发。每个线程都有一个与缺省通知中心(default notification center)相关的缺省通知队列(defaultQueue)。
另外NSNotificationQueue是依赖runloop
的,所以如果线程的runloop未开启则无效,至于为什么依赖runloop下面会解释
NSNotificationQueue主要做了两件事:
- 添加通知到队列
- 删除通知
// 往通知队列添加通知
- (void)enqueueNotification:(NSNotification *)notification
postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification
postingStyle:(NSPostingStyle)postingStyle
coalesceMask:(NSNotificationCoalescing)coalesceMask
forModes:(nullable NSArray<NSRunLoopMode> *)modes; // 如果modes为nil,则对于runloop的所有模式发送通知都是有效的
// 移除通知队列中的通知
- (void)dequeueNotificationsMatching:(NSNotification *)notification
coalesceMask:(NSUInteger)coalesceMask;
队列的合并策略和发送时机
把通知添加到队列等待发送,同时提供了一些附加条件供开发者选择,如:什么时候发送通知、如何合并通知等,系统给了如下定义
// 表示通知的发送时机
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // runloop空闲时发送通知
NSPostASAP = 2, // 尽快发送,这种情况稍微复杂,这种时机是穿插在每次事件完成期间来做的
NSPostNow = 3 // 立刻发送或者合并通知完成之后发送
};
// 通知合并的策略,有些时候同名通知只想存在一个,这时候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0, // 默认不合并
NSNotificationCoalescingOnName = 1, // 只要name相同,就认为是相同通知
NSNotificationCoalescingOnSender = 2 // object相同
};
NSNotification在多线程中的使用
无论在哪个线程中注册了观察者,通知的发送
和接收
都是在同一个线程
中。所以当接收到通知做UI操作的时候就需要考虑线程的问题。如果在子线程中接收到通知,需要切换到主线程
再做更新UI
的操作。
在主线程注册了观察者,然后在子线程发送通知,最后接收和处理通知也是在子线程。一般情况下,发送通知所在的线程就是接收通知所在的线程。
主线程响应通知
异步线程发送通知则响应函数也是在异步线程,如果执行UI刷新相关的话就会出问题,那么如何保证在主线程响应通知呢?
其实也是比较常见的问题了,基本上解决方式如下几种:
- 使用
addObserverForName: object: queue: usingBlock
方法注册通知,指定在mainqueue
上响应block - 在主线程注册一个machPort,它是用来做线程通信的,当在异步线程收到通知,然后给machPort发送消息,这样肯定是在主线程处理的,具体用法去网上资料很多,苹果官网也有
通知的实现原理
NSNotificationCenter
是通知的管理类,实现较复杂。NSNotificationCenter中主要定义了两个table,同时也定义了Observation保存观察者信息。它们结构体可以简化如下:
// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
Observation *wildcard; /* 链表结构,保存既没有name也没有object的通知 */
GSIMapTable nameless; /* 存储没有name但是有object的通知 */
GSIMapTable named; /* 存储带有name的通知,不管有没有object */
...
} NCTable;
// Observation 存储观察者和响应结构体,基本的存储单元
typedef struct Obs {
id observer; /* 观察者,接收通知的对象 */
SEL selector; /* 响应方法 */
struct Obs *next; /* Next item in linked list. */
...
} Observation;
在NSNotificationCenter
内部保存了两张表:一张用户保存添加观察者时传入了NotificationName的情况,一种用户保存添加观察者时没有传入NoficationName的情况。
Named Table
在named table中,NotificationName
作为表的key
,但因注册观察者的时可传入一个object
参数用于接收指定对象发出的通知,并且一个通知可注册多个观察者,所以还需要一张表来保存object
和observer
的对应关系。这张表以object为key,observer为value。如何实现同一个通知保存多个观察者的情况?答案就是用链表的数据结构
。
named table最终的数据结构如上图所示:
- 外层是一个table,以通知名称NotificationName为key,其value为一个table(简称内层table)。
- 内层table以object为key,其value为一个
链表
,用来保存所有的观察者。
注意
: 在实际开发过程中object参数我们经常传nil
,这时候系统会根据nil自动生成
一个key,相当于这个key对应的value(链表)保存的就是当前通知传入了NotificationName没有传入object的所有观察者。当对应的NotificationName的通知发送时,链表中所有的观察者都会收到通知。
Nameless Table
Nameless Table比Named Table要简单很多,因为没有NotificationName作为key,直接用object作为key。相较于Named Table要少一层table嵌套。
image.png
wildcard
wildcard是链表的数据结构,如果在注册观察者时既没有传入NotificationName,也没有传入object,就会添加到wildcard的链表中。注册到这里的观察者能接收到 所有的系统通知。
添加观察者流程
有了上面基本的结构关系,再来看添加过程就会很简单。在初始化NotificationCenter
时会创建一个对象
,这个对象里保存了Named Table
、Nameless Table
、wildcard
和一些其它信息。所有注册观察者的操作最后都会调用addObserver:selector:name:object:
。
- 首先会根据传入的参数实例化一个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对象作为头节点插入进去。
没有传入NotificationName的情况和上面的过程类似,只不过是直接根据对应的object为key去找对应的链表而已。如果既没有传入NotificationName也没有传入object,则这个观察者会添加到wildcard链表中。
发送通知流程
发送通知一般调用postNotificationName:object:userInfo:
来实现,内部会根据传入的参数实例化一个NSNotification
对象,包含name、object、userinfo等信息。
发送通知的流程总体来说是根据NotificationName和object找到对应的链表,然后遍历整个链表,给每个Observation节点中保存的oberver发送对应的SEL消息。
- 首先会创建一个数组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),调用形式如下:
[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中取出对应的链表,然后清空链表。