面试题:
一、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;
修改
- 告诉中心的数据结构
将告诉分了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 增加新的监听者逻辑
修改
- 注册监听: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,告诉的发送时同步的,还是异步的
3,NSNotificationCenter承受音讯和发送音讯是在一个线程里吗?怎么异步发送音讯
4,NSNotificationQueue是异步还是同步发送?在哪个线程呼应
5,NSNotificationQueue和runloop的联系
6,怎么保证告诉接纳的线程在主线程
7,页面销毁时不移除告诉会崩溃吗
8,屡次增加同一个告诉会是什么成果?屡次移除告诉呢