iOS NSNotification实现原理
通知概念
苹果官方文档有一段对通知的介绍如下:
A notification is a message sent to one or more observing objects to inform them of an event in a program. The notification mechanism of Cocoa follows a broadcast model.
通知机制的核心是一个与线程关联的单例对象叫通知中心(NSNotificationCenter)。通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。
NotificationCenter
NSNotification
NSNotification
包含了如下必要字段且均是只读的:
@property (readonly, copy) NSNotificationName name; // 通知名称,通知的唯一标识
@property (nullable, readonly, retain) id object; // 任意对象,通常是通知发送者
@property (nullable, readonly, copy) NSDictionary *userInfo; // 通知的附加信息
可以通过Designaged Initializer函数创建NSNotification
的实例对象:
// 指定初始化函数
- (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo NS_DESIGNATED_INITIALIZER;
也可以通过NSNotification (NSNotificationCreation)
分类相应的方法创建NSNotification
的实例对象。
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
- (instancetype)init /*NS_UNAVAILABLE*/; /* do not invoke; not a valid initializer for this class */
NSNotification
对象是不可变的。
NSNotification
不能通过init
初始化,否则会引起如下崩溃:
但是一般情况下不会直接这样创建通知对象。实际开发中更多的是直接调用NSNoficationCenter
的postNotificationName:object:
或postNotificationName:object:userInfo:
方法发送通知,这两个方法内部会根据传入的参数直接创建通知对象。
NSNotificationCenter
NSNotificationCenter
提供了一种互不相干的对象之间能够相互通信的方式。它接收NSNotification
对象并把通知广播给所有感兴趣的对象。
NSNotificationCenter
暴露给外部的属性只有一个defaultCenter
,而且这个属性还是只读的。
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
暴露给外部的方法分为三类:添加通知观察者的方法、发出通知的方法、移除通知观察者的方法。
// 添加通知观察者
- (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开始,即使不移除观察者对象,程序也不会出现异常。
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its
dealloc
method.这是因为在iOS9以后,通知中心持有的观察者由==unsafe_unretained==引用变为==weak==引用。即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空。但是通过
addObserverForName:object: queue:usingBlock:
方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。
NSNotificationQueue
NSNotificationQueue
通知队列充当通知中心的缓冲区。通知队列通常以FIFO
(先进先出)的顺序来维护通知。每个线程都有一个与缺省通知中心(default notification center)相关的缺省通知队列(defaultQueue)。
// 缺省的通知队列
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;
// 指定初始化函数
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
通过defaultQueue
获取默认的通知队列或者通过指定初始化函数initWithNotificationCenter:
创建通知队列,最终都是通过NSNotificationCenter
来发送、注册通知。
// 往通知队列添加通知
- (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;
合并通知
NSNotificationNoCoalescing
. 不合并队列中的通知NSNotificationCoalescingOnName
. 按通知名称合并通知NSNotificationCoalescingOnSender
. 按传入的object合并通知
发送方式
NSPostASAP
. 在当前通知调用结束或计时器超时发送通知NSPostWhenIdle
. 当runloop处于空闲状态时发送通知NSPostNow
. 在合并通知后立即发送通知
NSNotification在多线程中的使用
无论在哪个线程中注册了观察者,通知的发送和接收都是在同一个线程中。所以当接收到通知做UI操作的时候就需要考虑线程的问题。如果在子线程中接收到通知,需要切换到主线程再做更新UI的操作。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
static NSString * const NOTIFICATION_NAME = @"notification_name";
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:NOTIFICATION_NAME object:nil];
NSLog(@"Register Observer. Current thread = %@", [NSThread currentThread]);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Post Notification. Current thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:nil];
});
}
- (void)handleNotification:(NSNotification *)n {
NSLog(@"Received Notification. Current thread = %@", [NSThread currentThread]);
}
运行接口如下:
Register Observer. Current thread = <NSThread: 0x6000015fd3c0>{number = 1, name = main}
Post Notification. Current thread = <NSThread: 0x60000157cf00>{number = 5, name = (null)}
Received Notification. Current thread = <NSThread: 0x60000157cf00>{number = 5, name = (null)}
在主线程注册了观察者,然后在子线程发送通知,最后接收和处理通知也是在子线程。一般情况下,发送通知所在的线程就是接收通知所在的线程。
将通知重定向到指定线程
解决方法:捕获发送通知所在线程的通知,然后将其重定向至指定线程。关于通知重定向,官方文档给出了一种解决方法。
一种重定向通知的方式是自定义通知队列(不是
NSNotificationQueue
对象),让自定义队列去维护需要重定向的通知。仍然像之前一样注册通知的观察者,当接收到通知时,判断当前线程是否是我们期望的线程,如果不是,就将通知放到自定义队列中,然后发送一个信号sigal
到期望的线程中,告知这个线程需要处理通知。指定线程收到通知后,从自定义队列中把这个通知移除,并进行后续处理。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self setUpThreadingSupport];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:NOTIFICATION_NAME object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"Post Notification. Current thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:nil];
});
}
- (void)setUpThreadingSupport {
if (self.notifications) {
return;
}
self.notifications = [NSMutableArray array];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
[self.notificationPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:self.notificationPort forMode:(__bridge NSString *)kCFRunLoopCommonModes];
}
- (void)processNotification:(NSNotification *)n {
if ([NSThread currentThread] != self.notificationThread) {
[self.notificationLock lock];
[self.notifications addObject:n];
[self.notificationLock unlock];
[self.notificationPort sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];
} else {
// Process the notification here;
NSLog(@"Receive Notification. Current thread = %@", [NSThread currentThread]);
}
}
#pragma MacPort Delegate
- (void)handleMachMessage:(void *)msg {
[self.notificationLock lock];
while (self.notifications.count) {
NSNotification *n = [self.notifications objectAtIndex:0];
[self.notifications removeObjectAtIndex:0];
[self.notificationLock unlock];
[self processNotification:n];
[self.notificationLock lock];
}
[self.notificationLock unlock];
}
输出结果:
Post Notification. Current thread = <NSThread: 0x600002641300>{number = 3, name = (null)}
Receive Notification. Current thread = <NSThread: 0x60000261e940>{number = 1, name = main}
从运行结果可知,在子线程发送通知,在主线程接收和处理通知。当然这种实现方式也有限制:
- 所有线程的通知处理都必须通过相同的方法
processNotification:
- 每个对象必须提供自己的实现和通信端口
通知的实现原理
NSNotification
是一个类蔟不能够实例化,当我们调用initWithName:object:userInfo:
方法的时候,系统内部会实例化NSNotification
的子类NSConcreteNotification
。在这个子类中重写了NSNofication
定义的相关字段和方法。
NSNotificationCenter
是通知的管理类,实现较复杂。NSNotificationCenter
中主要定义了两个table,同时也定义了Observation
保存观察者信息。它们结构体可以简化如下:
typedef struct NCTbl {
Observation *wildcard; /* Get ALL messages. */
GSIMapTable nameless; /* Get messages for any name. */
GSIMapTable named; /* Getting named messages only. */
} NCTable;
typedef struct Obs {
id observer; /* Object to receive message. */
SEL selector; /* Method selector. */
struct Obs *next; /* Next item in linked list. */
int retained; /* Retain count for structure. */
struct NCTbl *link; /* Pointer back to chunk table */
} 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嵌套。
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中取出对应的链表,然后清空链表。
总结
其实上述分析通知的过程中仍有很多细节没有考虑到,比如在整个table非常大的时候是如何保证查询效率的。感兴趣的同学可以进行更深层次的研究。
参考文献
Notification
NSNotificationCenter
NSNotificationQueue
Delivering Notifications To Particular Threads
Gunstep NSNotififcationCenter sourcecode