面试题:
一、NSNotificationCenter 和 delegate 的区别
- 是使用 观察者模式 来实现的用于跨层传递消息的机制 (无需代理,双方无需建立关系)
- KVO也是观察者模式
- NSNotificationCenter 是使用观察者模式
- delegate是使用通知者
- NSNotificationCenter 是一对多
二、如何实现通知机制
- 通知中心维护一个MAP 表,NSNotificationName:观察者List
- 观察者List :包含 观察者 观察者要实现方法 以及回调方法的回调信息
通知的底层实现原理
1.通知的基础使用
- 平时常用的方式就是添加通知
- 适当的时机发送消息
- 在页面销毁时移除通知
@interface NSNotification : NSObject <NSCopying, NSCoding>
@property (readonly, copy) NSNotificationName name; // 通知的名字
@property (nullable, readonly, retain) id object; // 通知的发送者
@property (nullable, readonly, copy) NSDictionary *userInfo;// 通知携带的参数
- (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;
@end
@interface NSNotificationCenter : NSObject
...
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
// 监听来自指定 名字 & 发送者 的通知,接到通知后,指定观察者执行指定方法
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 监听来自指定 名字 & 发送者 的通知,接到通知后,指定队列执行block方法
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
// 发送通知
- (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;
...
@end
2.监听通知
由于苹果没有对相关源码开放,所以以GNUStep源码为基础进行研究,GNUStep虽然不是苹果官方的源码,但很具有参考意义。
我们知道NSNotification内有三个属性name名字、object发送者、传递的参数。
当我们调用通知中心 addObserver 的时候
- 如何存储观察者数据呢?
- 系统内部是如何存储所有NSNotification的数据?
1. 注册监听:addObserver: selector: name: object:
1. 观察者的数据结构
实际上是一个单向链表。一个结构体 Observation 包含了一个observer(观察者)、selector(该观察者需要执行的方法) 、以及指向下一个节点的指针。
// Observation 存储观察者和响应结构体,基本的存储单元
typedef struct Obs {
id observer; /* 观察者,接收通知的对象 */
SEL selector; /* 响应方法 */
struct Obs *next; /* Next item in linked list. */
...
} Observation;
2. 通知中心的数据结构
将通知分了3类存储:
●有名字的
●没名字有发送者的
●没名字也没有发送者的
分别对象 NCTbl 结构体里面的三个成员:
// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
Observation *wildcard; /* 单向链表结构,保存既没有name也没有object的通知,包含监听者和回调函数 */
GSIMapTable nameless; /* 存储没有name但是有object的通知 */
GSIMapTable named; /* 存储带有name的通知,不管有没有object */
...
} NCTable;
添加监听源码:
- (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 {
// 有节点的时候,获取第一个节点,
// 将第一个节点next 赋值给新的节点的next
// 将第一个节点的next 改为新的节点
// 也就是每个新的节点,都紧跟在第一个节点之后
// 也没有去重,只有是name/object 一致,就再创建一个新的节点
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;
}
}
2.1 有名字的通知的存储结构
2.2 没有名字的 & 有发送者的通知的存储结构
2.3 没有名字的 & 有发送者的通知的存储结构
2.4 增加新的监听者逻辑
2. 注册监听:addObserverForName:object: queue: usingBlock:
在这个方法中,是在上面👆的存储结构中增加了一个 GSNotificationObserver(代理观察者),它的内部保存了 queue 和 block , 收到消息的时候回调didReceiveNotification: 方法,在指定的 queue 中执行 block 。
@implementation GSNotificationObserver
{
NSOperationQueue *_queue; // 保存传入的队列
GSNotificationBlock _block; // 保存传入的block
}
...
// 响应接收通知的方法,并在指定队列中执行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
观察者的数据结构变成了这样:
3.总结
面试题:多次注册为什么可以收到多次通知?
因为当name / object 一致时,就会创建一个新的观察者结构体,插入到链表中,没有判断去重。
3.发送通知
发送的过程是查找所有观察者(不管是否重复),然后让观察者 observer 执行方法sel。
- 通过 name & bject 查找到所有的 obs 对象(保存了 observer 和 sel),放到数组中
- 通过 performSelector:逐一调用 sel,这是个同步操作
- 释放 notification 对象
4.移除通知
按照 name / object / observer 移除监听:
- 根据指定 name 查找 map, 找到对应的 value map
- 根据指定 object 查找 value map, 找到所有的观察者
- 遍历观察者,找到指定的observer,然后移除这个结构体,如果有重复的也会被移除掉
5.通知队列
通知队列是一个双向链表实现的队列,用来存储通知的,它有向队列中添加及移除通知的API。
- 通知队列发送时机有三种:
-
- runloop: 会被添加到_idleQueue,空闲时发送通知,
-
-
- 监听运行时状态,如果处于完全空闲就调用 GSPrivateNotifyIdle()
- 对于主线程来讲,无需手动启动 runloop, 通知自会发送
- 对于子线程就需要启动运行循环,通知才会发送。不然等子线程执行完任务直接退出了,就无法发送通知了。
-
-
- 尽快发送:会被添加到_asapQueue, 尽快发送通知,
-
-
- 监听运行时状态,如果处于稍有空闲就立刻调用 GSPrivateNotifyIdle()
-
-
- 立刻发送或者合并通知完成之后发送
-
-
- 调用通知中心,直接发送消息
-
- 可以设置发送时机来实现异步发送通知,但是从线程角度看,并不是真正的异步发送,而是借助于 runloop 机制延迟发送。
// 表示通知的发送时机
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // runloop空闲时发送通知,会被添加到_idleQueue
NSPostASAP = 2, // 尽快发送,这种情况稍微复杂,这种时机是穿插在每次事件完成期间来做的,会被添加到_asapQueue
NSPostNow = 3 // 立刻发送或者合并通知完成之后发送
};
// 通知合并的策略
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0, // 默认不合并
NSNotificationCoalescingOnName = 1, // 只要name相同,就认为是相同通知
NSNotificationCoalescingOnSender = 2 // 按发送者合并
};
@interface NSNotificationQueue : NSObject {
@private
id _notificationCenter; // 持有一个通知中心对象
id _asapQueue; // 尽快发送的通知队列
id _asapObs; // 尽快发送的观察者observer
id _idleQueue; // 空闲时发送的通知队列
id _idleObs; // 空闲时发送的观察者
}
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
// 向队列里面加入通知, 并且指定发送时机
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 向队列里面加入通知, 并且指定发送时机、合并策略,运行循环模式
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;
// 移除通知
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
@end
异步线程发送通知则响应函数也是在异步线程,如果执行UI刷新相关的话就会出问题,那么如何保证在主线程响应通知呢?
其实也是比较常见的问题了,基本上解决方式如下几种:
1使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block
2在主线程注册一个machPort,它是用来做线程通信的,当在异步线程收到通知,然后给machPort发送消息,这样肯定是在主线程处理的。
1实现原理(结构设计、通知如何存储的、name&observer&SEL之间的关系等)
2通知的发送时同步的,还是异步的
3NSNotificationCenter接受消息和发送消息是在一个线程里吗?如何异步发送消息
4NSNotificationQueue是异步还是同步发送?在哪个线程响应
5NSNotificationQueue和runloop的关系
6如何保证通知接收的线程在主线程
7页面销毁时不移除通知会崩溃吗
8多次添加同一个通知会是什么结果?多次移除通知呢