面试题:

一、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

NSNotificationCenter面试题简单整理

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;

NSNotificationCenter面试题简单整理

修改

  1. 告诉中心的数据结构
    将告诉分了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 有姓名的告诉的存储结构

NSNotificationCenter面试题简单整理

修改

2.2 没有姓名的 & 有发送者的告诉的存储结构

NSNotificationCenter面试题简单整理

修改

2.3 没有姓名的 & 有发送者的告诉的存储结构

NSNotificationCenter面试题简单整理

修改

2.4 增加新的监听者逻辑

NSNotificationCenter面试题简单整理

修改

  1. 注册监听: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

观察者的数据结构变成了这样:

NSNotificationCenter面试题简单整理

修改

3.总结

面试题:屡次注册为什么能够收到屡次告诉?

因为当name / object 共同时,就会创立一个新的观察者结构体,刺进到链表中,没有判别去重。

3.发送告诉

发送的进程是查找一切观察者(不论是否重复),然后让观察者 observer 履行办法sel。

  1. 经过 name & bject 查找到一切的 obs 目标(保存了 observer 和 sel),放到数组中
  2. 经过 performSelector:逐个调用 sel,这是个同步操作
  3. 释放 notification 目标

4.移除告诉

按照 name / object / observer 移除监听:

  1. 依据指定 name 查找 map, 找到对应的 value map
  2. 依据指定 object 查找 value map, 找到一切的观察者
  3. 遍历观察者,找到指定的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,屡次增加同一个告诉会是什么成果?屡次移除告诉呢