likes
comments
collection
share

NSNotificationCenter面试题简单整理

作者站长头像
站长
· 阅读数 15

面试题:

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

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

编辑

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,多次添加同一个通知会是什么结果?多次移除通知呢