iOS之深入解析通知NSNotification的底层原理
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析通知NSNotification的底层原理相关的知识,希望对你有一定的参考价值。
一、概念
① NSNotification
- NSNotification 用于描述通知的类,一个 NSNotification 对象就包含了一条通知的信息,NSNotification 对象是不可变的。所以当创建一个通知时通常包含如下属性:
@interface NSNotification : NSObject <NSCopying, NSCoding>
@property (readonly, copy) NSNotificationName name;
@property (nullable, readonly, retain) id object;
@property (nullable, readonly, copy) NSDictionary *userInfo;
@end
- NSNotification 可以通过 Designaged Initializer 函数创建实例对象:
- (instancetype)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;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
- 也可以通过NSNotification (NSNotificationCreation) 分类相应的方法创建 NSNotification 的实例对象:
@interface NSNotification (NSNotificationCreation)
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
- (instancetype)init /*API_UNAVAILABLE(macos, ios, watchos, tvos)*/; /* do not invoke; not a valid initializer for this class */
@end
- 实际开发中更多的是直接调用 NSNoficationCenter 的 postNotificationName:object: 或 postNotificationName:object:userInfo: 方法发送通知,这两个方法内部会根据传入的参数直接创建通知对象。
② NSNoficationCenter
- 通知机制的核心是一个与线程关联的单例对象叫通知中心 NSNotificationCenter。通知中心发送通知给观察者是同步的,也可以用通知队列 NSNotificationQueue 异步发送通知。
- NSNotificationCenter 提供了一种互不相干的对象之间能够相互通信的方式,它接收 NSNotification 对象并把通知广播给所有感兴趣的对象。NSNotificationCenter 的属性只有一个 defaultCenter,而且这个属性是只读的:
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
- NSNoficationCenter 是一个单例类,负责管理通知的创建和发送,属于最核心的类了。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
- 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;
- NSNotificationQueue 是依赖 runloop 的,所以如果线程的 runloop 未开启则无效。NSNotificationQueue 主要做了两件事:添加通知到队列、删除通知。核心 API 如下:
// 把通知添加到队列中,NSPostingStyle是个枚举,下面会介绍
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 删除通知,把满足合并条件的通知从队列中删除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
- 合并通知:
-
- NSNotificationNoCoalescing 不合并队列中的通知;
-
- NSNotificationCoalescingOnName 按通知名称合并通知;
-
- NSNotificationCoalescingOnSender 按传入的 object 合并通知。
- 发送方式:
-
- NSPostASAP 在当前通知调用结束或计时器超时发送通知;
-
- NSPostWhenIdle 当 runloop 处于空闲状态时发送通知;
-
- NSPostNow 在合并通知后立即发送通知。
- 队列的合并策略和发送时机:把通知添加到队列等待发送,同时提供了一些附加条件供开发者选择,如:什么时候发送通知、如何合并通知等,系统给了如下定义:
// 表示通知的发送时机
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相同
};
- GSNotificationObserver 类是 GNUStep 源码中定义的,它的作用是代理观察者,主要用来实现接口:addObserverForName:object: queue: usingBlock: 时用到,即要实现在指定队列回调 block,那么 GSNotificationObserver 对象保存了 queue 和 block 信息,并且作为观察者注册到通知中心,等到接收通知时触发了响应方法,并在响应方法中把 block 抛到指定 queue 中执行,定义如下:
@implementation GSNotificationObserver {
NSOperationQueue *_queue; // 保存传入的队列
GSNotificationBlock _block; // 保存传入的block
}
- (id) initWithQueue: (NSOperationQueue *)queue
block: (GSNotificationBlock)block {
// ......初始化操作
}
- (void) dealloc {
//....
}
// 响应接收通知的方法,并在指定队列中执行block
- (void) didReceiveNotification: (NSNotification *)notif {
if (_queue != nil) {
GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc]
initWithNotification: notif block: _block];
[_queue addOperation: op];
} else {
CALL_BLOCK(_block, notif);
}
}
@end
二、底层实现
① 通知结构分析
- 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 中,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 比 Named Table 要简单很多,因为没有 NotificationName 作为 key,直接用 object 作为 key,相较于 Named Table 要少一层 table 嵌套。Nameless Table 如下所示:
- wildcard 是链表的数据结构,如果在注册观察者时既没有传入 NotificationName,也没有传入 object,就会添加到 wildcard 的链表中。注册到这里的观察者能接收到 所有的系统通知。
② 注册通知分析
- 在初始化 NotificationCenter 时会创建一个对象,这个对象里保存了 Named Table、Nameless Table、wildcard 和一些其它信息。所有注册观察者的操作最后都会调用 addObserver:selector:name:object:。源码实现如下:
/*
observer:观察者,即通知的接收者
selector:接收到通知时的响应方法
name: 通知name
object:携带对象
*/
- (void) addObserver: (id)observer
selector: (SEL)selector
name: (NSString*)name
object: (id)object {
// 前置条件判断
......
// 创建一个observation对象,持有观察者和SEL,下面进行的所有逻辑就是为了存储它
o = obsNew(TABLE, selector, observer);
/*======= case1: 如果name存在 =======*/
if (name) {
//-------- NAMED是个宏,表示名为named字典。以name为key,从named表中获取对应的mapTable
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
if (n == 0) { // 不存在,则创建
m = mapNew(TABLE); // 先取缓存,如果缓存没有则新建一个map
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
...
}
else { // 存在则把值取出来 赋值给m
m = (GSIMapTable)n->value.ptr;
}
//-------- 以object为key,从字典m中取出对应的value,其实value被MapNode的结构包装了一层,这里不追究细节
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n == 0) {// 不存在,则创建
o->next = ENDOBS;
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
}
else {
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
}
/*======= case2:如果name为空,但object不为空 =======*/
else if (object) {
// 以object为key,从nameless字典中取出对应的value,value是个链表结构
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
// 不存在则新建链表,并存到map中
if (n == 0) {
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}
else { // 存在 则把值接到链表的节点上
...
}
}
/*======= case3:name 和 object 都为空 则存储到wildcard链表中 =======*/
else {
o->next = WILDCARD;
WILDCARD = o;
}
}
- 源码分析:
-
- NCTable 结构体中核心的三个变量以及功能:wildcard、named、nameless,在源码中直接用宏定义表示了:WILDCARD、NAMELESS、NAMED。
-
- case1: 存在 name(无论 object 是否存在):
-
-
- 注册通知,如果通知的 name 存在,则以 name 为 key 从 named 字典中取出值 n(这个 n 其实被 MapNode 包装了一层,便于理解这里直接认为没有包装),这个 n 还是个字典,各种判空新建逻辑不讨论;
-
-
-
- 然后以 object 为 key,从字典 n 中取出对应的值,这个值就是 Observation 类型(后面简称 obs)的链表,然后把刚开始创建的 obs 对象 o 存储进去;
-
-
- 如果注册通知时传入 name,那么会是一个双层的存储结构:
-
-
- 找到 NCTable 中的 named 表,这个表存储了还有 name 的通知
以 name 作为 key,找到 value,这个 value 依然是一个 map;
- 找到 NCTable 中的 named 表,这个表存储了还有 name 的通知
-
-
-
- map 的结构是以 object 作为 key,obs 对象为 value,这个 obs 对象的结构上面已经解释,主要存储了observer & SEL;
-
-
- case2: 只存在 object:
-
-
- 以 object 为 key,从 nameless 字典中取出 value,此 value 是个 obs 类型的链表;
-
-
-
- 把创建的 obs 类型的对象 o 存储到链表中;
-
-
-
- 只存在 object 时存储只有一层,那就是 object 和 obs 对象之间的映射。
-
-
- case3:没有 name 和 object:直接把 obs 对象存放在了 Observation *wildcard 链表结构中。
- 流程总结:
-
- 首先会根据传入的参数实例化一个 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 链表中。
- addObserverForName 实现的功能是在接收到通知时,在指定队列 queue 执行 block:
// 这个api使用频率较低,怎么实现在指定队列回调block的,值得研究
- (id) addObserverForName: (NSString *)name
object: (id)object
queue: (NSOperationQueue *)queue
usingBlock: (GSNotificationBlock)block
{
// 创建一个临时观察者
GSNotificationObserver *observer =
[[GSNotificationObserver alloc] initWithQueue: queue block: block];
// 调用了接口1的注册方法
[self addObserver: observer
selector: @selector(didReceiveNotification:)
name: name
object: object];
return observer;
}
- 源码分析:
-
- 创建一个 GSNotificationObserver 类型的对象 observer,并把 queue 和 block 保存下来;
-
- 调用 addObserver 进行通知的注册;
-
- 接收到通知时会响应 observer 的 didReceiveNotification: 方法,然后在 didReceiveNotification: 中把 block 抛给指定的 queue 去执行。
③ 发送通知分析
- 发送通知一般调用 postNotificationName:object:userInfo: 来实现,内部会根据传入的参数实例化一个 NSNotification 对象,包含 name、object、userinfo 等信息。发送通知的源码实现基本上就是查找和调用响应方法,核心函数如下:
// 发送通知
- (void) postNotificationName: (NSString*)name
object: (id)object
userInfo: (NSDictionary*)info {
// 构造一个GSNotification对象, GSNotification继承了NSNotification
GSNotification *notification;
notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
notification->_name = [name copyWithZone: [self zone]];
notification->_object = [object retain];
notification->_info = [info retain];
// 进行发送操作
[self _postAndRelease: notification];
}
// 发送通知的核心函数,主要做了三件事:查找通知、发送、释放资源
- (void) _postAndRelease: (NSNotification*)notification {
// step1: 从named、nameless、wildcard表中查找对应的通知
...
// step2:执行发送,即调用performSelector执行响应方法,从这里可以看出是同步的
while (count-- > 0) {
o = GSIArrayItemAtIndex(a, count).ext;
if (o->next != 0) {
NS_DURING {
[o->observer performSelector: o->selector
withObject: notification];
}
NS_HANDLER
{
BOOL logged;
/* Try to report the notification along with the exception,
* but if there's a problem with the notification itself,
* we just log the exception.
*/
NS_DURING
NSLog(@"Problem posting %@: %@", notification, localException);
logged = YES;
NS_HANDLER
logged = NO;
NS_ENDHANDLER
if (NO == logged)
{
NSLog(@"Problem posting notification: %@", localException);
}
}
NS_ENDHANDLER
}
}
lockNCTable(TABLE);
GSIArrayEmpty(a);
unlockNCTable(TABLE);
// step3: 释放资源
RELEASE(notification);
}
- 源码说明:
-
- 通过 namoe&bject 查找到所有的 obs 对象(保存了 observer 和 sel),放到数组中;
-
- 通过 performSelector:逐一调用 sel,这是个同步操作;
-
- 释放 notification 对象。
- 流程总结:
-
- 首先会创建一个数组 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 中取出对应的链表,然后清空链表。
- (void) removeObserver: (id)observer
name: (NSString*)name
object: (id)object
{
if (name == nil && object == nil && observer == nil)
return;
/*
* NB. The removal algorithm depends on an implementation characteristic
* of our map tables - while enumerating a table, it is safe to remove
* the entry returned by the enumerator.
*/
lockNCTable(TABLE);
if (name == nil && object == nil)
{
WILDCARD = listPurge(WILDCARD, observer);
}
if (name == nil)
{
GSIMapEnumerator_t e0;
GSIMapNode n0;
/*
* First try removing all named items set for this object.
*/
e0 = GSIMapEnumeratorForMap(NAMED);
n0 = GSIMapEnumeratorNextNode(&e0);
while (n0 != 0)
{
GSIMapTable m = (GSIMapTable)n0->value.ptr;
NSString *thisName = (NSString*)n0->key.obj;
n0 = GSIMapEnumeratorNextNode(&e0);
if (object == nil)
{
GSIMapEnumerator_t e1 = GSIMapEnumeratorForMap(m);
GSIMapNode n1 = GSIMapEnumeratorNextNode(&e1);
/*
* Nil object and nil name, so we step through all the maps
* keyed under the current name and remove all the objects
* that match the observer.
*/
while IOS NSNotification Center 通知中心的使用