从底层了解面试题-NSNotification篇

一.模型结构

1.NSNotification

@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 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
复制代码

接收通知时会返回这个对象

2.NSNotificationCenter

@interface NSNotificationCenter : NSObject {
    @package
    void *_impl;
    void *_callback;
    void *_pad[11];
}

// 默认的通知中心,全局唯一
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
// 在通知中心注册一个观察者
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 发送一个通知
- (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;
// 在通知中心注册一个观察者,block回调的形式代替selector
- (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));
    // The return value is retained by the system, and should be held onto by the caller in
    // order to remove the observer with removeObserver: later, to stop observation.

@end
复制代码

是一个单例类,负责管理通知的创建和发送,主要做三件事

  • 注册通知
  • 发送通知
  • 移除通知

注册通知目前有两种方式:selectorblock

//注册观察者
- (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 API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
复制代码

对于添加指定观察者对象的方式,observer不能为nilblock方式会执行copy方法,返回的是使用的匿名观察者对象,且指定观察者处理消息的操作对象NSOperationQueue

对于指定的消息名称name及发送者对象object都可以为空,即接收所有消息及所有发送对象发送的消息;若指定其中之一或者都指定,则表示接收指定消息名称及发送者的消息;

对于block方式指定的queue队列可为nil,则默认在发送消息线程处理;若指定主队列,即主线程处理,避免执行UI操作导致异常;

注意:注册观察者通知消息应避免重复注册,会导致重复处理通知消息,且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;
复制代码

可以通过NSNotification包装的通知消息对象发送消息,也可以分别指定消息名称、发送者及携带的信息来发送,且为同步执行模式,需要等待所有注册的观察者处理完成该通知消息,方法才会返回继续往下执行,且对于block形式处理通知对象是在注册消息指定的队列中执行,对于非block方式是在同一线程处理。

注意:消息发送类型需要与注册时类型一致,即若注册观察者同时指定了消息名称及发送者,则发送消息也需要同时指定,否则无法接收到消息

移除通知

//移除观察者
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
复制代码

可移除指定的观察者所有通知消息,即该观察者不再接收任何消息,一般用于观察者对象dealloc释放后调用,但在ios9macos10.11之后不需要手动调用,dealloc已经自动处理;

3.NSNotificationQueue

@interface NSNotificationQueue : NSObject {
@private
    id		_notificationCenter;
    id		_asapQueue;
    id		_asapObs;
    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
复制代码

可以通过defaultQueue获取当前线程绑定的通知消息队列,也可以通过initWithNotificationCenter:来指定通知管理中心

该对象主要做两件事

  1. 添加通知到队列
  2. 删除通知

通知发送时机

// 通知的发送时机
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空闲时发送通知
    NSPostASAP = 2,     // 在当前通知调用或者计时器结束发出通知
    NSPostNow = 3       // 立刻发送或在合并通知完成之后发送
};
复制代码

通知合并策略

// 通知合并的策略,有些时候同名通知只想存在一个,这时候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0,// 默认不合并
    NSNotificationCoalescingOnName = 1,// 按照通知名字合并通知
    NSNotificationCoalescingOnSender = 2// 按照传入的object合并通知
};
复制代码

异步通知的使用

NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP];
复制代码

通知合并的使用

// 发送多个通知
NSNotification *noti = [NSNotification notificationWithName:@"NSNotification" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP coalesceMask:NSNotificationNoCoalescing forModes:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostNow coalesceMask:NSNotificationNoCoalescing forModes:nil];

// 注册名为NSNotification的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:*@selector(noti:) name:@"NSNotification" object:nil];

- (void) noti:(NSNotification *) noti {
    NSLog(@"noti");
}
复制代码

coalesceMask 设置为 NSNotificationNoCoalescing 结果打印了三个noti 当 coalesceMask 设置为 NSNotificationCoalescingOnName 结果打印了一个noti

4.GSNotificationObserver

@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

复制代码

这个类是GNUStep源码中定义的,它的作用是代理观察者,主要用来实现接口:addObserverForName:object: queue: usingBlock:时用到,即要实现在指定队列回调block,那么GSNotificationObserver对象保存了queueblock信息,并且作为观察者注册到通知中心,等到接收通知时触发了响应方法,并在响应方法中把block抛到指定queue中执行

二.底层分析

1._GSIMapTable

通知的底层离不开这张映射表

*  A rough picture is include below:
 *   
 *  
 *   This is the map                C - array of the buckets
 *   +---------------+             +---------------+
 *   | _GSIMapTable  |      /----->| nodeCount     |  
 *   |---------------|     /       | firstNode ----+--\  
 *   | buckets    ---+----/        | ..........    |  |
 *   | bucketCount  =| size of --> | nodeCount     |  |
 *   | nodeChunks ---+--\          | firstNode     |  |
 *   | chunkCount  =-+\ |          |     .         |  | 
 *   |   ....        || |          |     .         |  |
 *   +---------------+| |          | nodeCount     |  |
 *                    | |          | fistNode      |  | 
 *                    / |          +---------------+  | 
 *         ----------   v                             v
 *       /       +----------+      +---------------------------+ 
 *       |       |  * ------+----->| Node1 | Node2 | Node3 ... | a chunk
 *   chunkCount  |  * ------+--\   +---------------------------+
 *  is size of = |  .       |   \  +-------------------------------+
 *               |  .       |    ->| Node n | Node n + 1 | ...     | another
 *               +----------+      +-------------------------------+
 *               array pointing
 *               to the chunks

复制代码

结构定义

#if	!defined(GSI_MAP_TABLE_T)
typedef struct _GSIMapBucket GSIMapBucket_t;
typedef struct _GSIMapNode GSIMapNode_t;

typedef GSIMapBucket_t *GSIMapBucket;
typedef GSIMapNode_t *GSIMapNode;
#endif

struct	_GSIMapNode {
  GSIMapNode	nextInBucket;	/* Linked list of bucket.	*/
  GSIMapKey	key;
#if	GSI_MAP_HAS_VALUE
  GSIMapVal	value;
#endif
};

struct	_GSIMapBucket {
  uintptr_t	nodeCount;	/* Number of nodes in bucket.	*/
  GSIMapNode	firstNode;	/* The linked list of nodes.	*/
};

#if	defined(GSI_MAP_TABLE_T)
typedef GSI_MAP_TABLE_T	*GSIMapTable;
#else
typedef struct _GSIMapTable GSIMapTable_t;
typedef GSIMapTable_t *GSIMapTable;

struct	_GSIMapTable {
  NSZone	*zone;
  uintptr_t	nodeCount;	/* Number of used nodes in map.	*/
  uintptr_t	bucketCount;	/* Number of buckets in map.	*/
  GSIMapBucket	buckets;	/* Array of buckets.		*/
  GSIMapNode	freeNodes;	/* List of unused nodes.	*/
  uintptr_t	chunkCount;	/* Number of chunks in array.	*/
  GSIMapNode	*nodeChunks;	/* Chunks of allocated memory.	*/
  uintptr_t	increment;
#ifdef	GSI_MAP_EXTRA
  GSI_MAP_EXTRA	extra;
#endif
};

复制代码

解析 _GSIMapTable 数据结构

1.从bucket的角度,看成一个单向链表

  • buckets是一个单向链表,存储着GSIMapBucketGSIMapBucket中其firstNode->nextInBucket表示下一个bucketfirstNode表示另一条单链表的首个元素。
  • bucketCount表示buckets的数量

2.从chunk角度,看成一个数组指针

  • nodeChunks 表示一个数组指针,数组存储所有单链表的首个元素node
  • chunkCount 表示数组大小

此外freeNodes则就是需要释放的元素,是一个单向链表。
其实就是一个hash表结构,既可以以数组的形式取到每个单向链表首元素,也可以以链表形式取得。
通过数组能够方便取到每个单向链表,再利用链表结构方便增删。

//取单向链表
//通过hash值%最大个数 来获取index,然后取出单向链表,再进行链表增删
GS_STATIC_INLINE GSIMapBucket
GSIMapPickBucket(unsigned hash, GSIMapBucket buckets, uintptr_t bucketCount) {
  return buckets + hash % bucketCount;
}

GS_STATIC_INLINE GSIMapBucket
GSIMapBucketForKey(GSIMapTable map, GSIMapKey key) {
  return GSIMapPickBucket(GSI_MAP_HASH(map, key),
    map->buckets, map->bucketCount);
}

//增删元素
GS_STATIC_INLINE void
GSIMapLinkNodeIntoBucket(GSIMapBucket bucket, GSIMapNode node) {
  node->nextInBucket = bucket->firstNode;
  bucket->firstNode = node;
}

GS_STATIC_INLINE void
GSIMapUnlinkNodeFromBucket(GSIMapBucket bucket, GSIMapNode node) {
  if (node == bucket->firstNode) {
      bucket->firstNode = node->nextInBucket;
  } else {
      GSIMapNode tmp = bucket->firstNode;
      while (tmp->nextInBucket != node) {
	  tmp = tmp->nextInBucket;
	}
      tmp->nextInBucket = node->nextInBucket;
    }
  node->nextInBucket = 0;
}
复制代码

这是OC中实现hash表的一种形式。但对于Javahashmap来说,更为简单了。hashmap涉及红黑树等查找问题,后续贴出对比下。

2.存储容器NCTbl

通知全局对象表结构

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
    Observation		*wildcard;	/* 链表结构,保存既没有name也没有object的通知*/
    GSIMapTable		nameless;	/* 存储没有name但是有object的通知*/
    GSIMapTable		named;		/* 存储带有name的通知,不管有没有object*/
    unsigned		lockCount;	/* Count recursive operations.	*/
    NSRecursiveLock	*_lock;		/* Lock out other threads.	*/
    Observation		*freeList;
    Observation		**chunks;
    unsigned		numChunks;
    GSIMapTable		cache[CACHESIZE];
    unsigned short	chunkIndex;
    unsigned short	cacheIndex;
} NCTable;

// Observation 存储观察者和响应结构体,基本的存储单元
typedef struct Obs { 
    id observer;      /* 观察者,接收通知的对象 */ 
    SEL selector;     /* 响应方法 */ 
    struct Obs *next; /* Next item in linked list. */ 
    ... 
} Observation;

复制代码

保存含有通知名称的通知表named需要注册object对象,因此该表结构体通过传入的name作为key,其中value同时也为GSIMapTable表用于存储对应的object对象的observer对象;

对没有传入通知名称只传入object对象的通知表nameless而言,只需要保存objectobserver的对应关系,因此object作为keyobserver作为value

3.注册通知

核心源码- Selector 形式

/*
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结构体中核心的三个变量以及功能:wildcardnamednameless,在源码中直接用宏定义表示了:WILDCARDNAMELESSNAMED

case1: 存在name(无论object是否存在)

  1. 注册通知,如果通知的name存在,则以name为key从named字典中取出值n(这个n其实被MapNode包装了一层,便于理解这里直接认为没有包装),这个n还是个字典,各种判空新建逻辑不讨论
  2. 然后以object为key,从字典n中取出对应的值,这个值就是Observation类型(后面简称obs)的链表,然后把刚开始创建的obs对象o存储进去

数据结构关系图: image.png

如果注册通知时传入name,那么会是一个双层的存储结构

  1. 找到NCTable中的named表,这个表存储了还有name的通知
  2. name作为key,找到value,这个value依然是一个map
  3. map的结构是以object作为key,obs对象为value,这个obs对象的结构上面已经解释,主要存储了observer & SEL

case2: 只存在object

  1. object为key,从nameless字典中取出value,此value是个obs类型的链表
  2. 把创建的obs类型的对象o存储到链表中

数据结构关系图:
image.png
只存在object时存储只有一层,那就是objectobs对象之间的映射

case3: 没有name和object

这种情况直接把obs对象存放在了Observation  *wildcard  链表结构中

核心源码- block 形式

block形式只是 Selector 函数的包装

- (id) addObserverForName: (NSString *)name 
                   object: (id)object 
                    queue: (NSOperationQueue *)queue 
               usingBlock: (GSNotificationBlock)block {
    // 创建一个临时观察者
    GSNotificationObserver *observer = 
        [[GSNotificationObserver alloc] initWithQueue: queue block: block];
    // 调用了selector的注册方法
    [self addObserver: observer 
             selector: @selector(didReceiveNotification:) 
                 name: name 
               object: object];
    return observer;
}

- (id) initWithQueue: (NSOperationQueue *)queue 
               block: (GSNotificationBlock)block {
    self = [super init];
    if (self == nil)
        return nil;
    ASSIGN(_queue, queue);
    _block = Block_copy(block);
    return self;
}

- (void) didReceiveNotification: (NSNotification *)notif {
    if (_queue != nil) {
        GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc] 
            initWithNotification: notif block: _block];
        [_queue addOperation: op];
    } else {
        CALL_BLOCK(_block, notif);
    }
}

复制代码

这个接口依赖于selector,只是多了一层代理观察者GSNotificationObserver

  1. 创建一个GSNotificationObserver类型的对象observer,并把queueblock保存下来
  2. 调用接口1进行通知的注册
  3. 接收到通知时会响应observerdidReceiveNotification:方法,然后在didReceiveNotification:中把block抛给指定的queue去执行

4.发送通知

核心代码

// 发送通知
- (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执行响应方法,从这里可以看出是同步的
   	[o->observer performSelector: o->selector
                    withObject: notification];
	//step3: 释放资源
    RELEASE(notification);
}
复制代码

上述代码主要做了三件事

  1. 通过name & object 查找到所有的obs对象(保存了observersel),放到数组中
  2. 通过performSelector:逐一调用sel,这是个同步操作
  3. 释放notification对象

源码逻辑可以看出发送过程的概述

  1. 从三个存储容器中:namednamelesswildcard去查找对应的obs对象
  2. 然后通过performSelector:逐一调用响应方法,这就完成了发送流程

5.删除通知

  1. 查找时仍然以nameobject为维度的,再加上observer做区分
  2. 因为查找时做了这个链表的遍历,所以删除时会把重复的通知全都删除掉
// 删除已经注册的通知
- (void) removeObserver: (id)observer
		   name: (NSString*)name
                 object: (id)object {
  if (name == nil && object == nil && observer == nil)
      return;
      ...
}

- (void) removeObserver: (id)observer{
  if (observer == nil)
      return;
  [self removeObserver: observer name: nil object: nil];
}
复制代码

6.异步通知

NSNotificationQueue的异步发送,从线程的角度看并不是真正的异步发送,或可称为延时发送,它是利用了runloop的时机来触发的

逻辑

  1. 根据coalesceMask参数判断是否合并通知
  2. 接着根据postingStyle参数,判断通知发送的时机,如果不是立即发送则把通知加入到队列中:_asapQueue_idleQueue

核心点

  1. 队列是双向链表实现
  2. 当postingStyle值是立即发送时,调用的是NSNotificationCenter进行发送的,所以NSNotificationQueue还是依赖NSNotificationCenter进行发送
/*
* 把要发送的通知添加到队列,等待发送
* NSPostingStyle 和 coalesceMask在上面的类结构中有介绍
* modes这个就和runloop有关了,指的是runloop的mode
*/ 
- (void) enqueueNotification: (NSNotification*)notification
		postingStyle: (NSPostingStyle)postingStyle
		coalesceMask: (NSUInteger)coalesceMask
		    forModes: (NSArray*)modes {
	......
  // 判断是否需要合并通知
  if (coalesceMask != NSNotificationNoCoalescing) {
      [self dequeueNotificationsMatching: notification
			    coalesceMask: coalesceMask];
  }
  switch (postingStyle) {
      case NSPostNow: {
      	...
      	// 如果是立马发送,则调用NSNotificationCenter进行发送
	     [_center postNotification: notification];
         break;
	  }
      case NSPostASAP:
      	// 添加到_asapQueue队列,等待发送
		add_to_queue(_asapQueue, notification, modes, _zone);
		break;

      case NSPostWhenIdle:
        // 添加到_idleQueue队列,等待发送
		add_to_queue(_idleQueue, notification, modes, _zone);
		break;
    }
}
复制代码

异步发送通知

  • runloop触发某个时机,调用GSPrivateNotifyASAP()GSPrivateNotifyIdle()方法,这两个方法最终都调用了notify()方法

  • notify()所做的事情就是调用NSNotificationCenterpostNotification:进行发送通知

static void notify(NSNotificationCenter *center, 
                   NSNotificationQueueList *list,
                   NSString *mode, NSZone *zone) {
 	......
    // 循环遍历发送通知
    for (pos = 0; pos < len; pos++) {
	  NSNotification *n = (NSNotification*)ptr[pos];
	  [center postNotification: n];
	  RELEASE(n);
	}
	......	
}

// 发送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode) {
    notify(item->queue->_center,
        item->queue->_asapQueue,
        mode,
        item->queue->_zone);
}

// 发送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode) {
    notify(item->queue->_center,
    	item->queue->_idleQueue,
    	mode,
    	item->queue->_zone);
}
复制代码

对于NSNotificationQueue总结如下

  1. 依赖runloop,所以如果在其他子线程使用NSNotificationQueue,需要开启runloop
  2. 最终还是通过NSNotificationCenter进行发送通知,所以这个角度讲它还是同步的
  3. 所谓异步,指的是非实时发送而是在合适的时机发送,并没有开启异步线程

主线程响应异步通知

异步线程发送通知则响应函数也是在异步线程,如果执行UI刷新相关的话就会出问题,那么如何保证在主线程响应通知呢?

其实也是比较常见的问题了,基本上解决方式如下几种:

  1. 使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block
 // 代码
@interface A : NSObject <NSMachPortDelegate>
- (void)test;
@end
@implementation A
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)test {
    [[NSNotificationCenter defaultCenter] addObserverForName:@"111" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"current thread %@ 刷新UI", [NSThread currentThread]);
        // 刷新UI ...
    }];
}
@end
 
// 输出
current thread <NSThread: 0x7bf29110>{number = 3, name = (null)}
2017-02-27 11:53:46.531 notification[29510:12833116] current thread <NSThread: 0x7be1d6f0>{number = 1, name = main} 刷新UI
复制代码
  1. 在主线程注册一个machPort,它是用来做线程通信的,当在异步线程收到通知,然后给machPort发送消息,这样肯定是在主线程处理的,具体用法
// 代码
#import <CoreFoundation/CFRunLoop.h>
@interface A : NSObject <NSMachPortDelegate>
@property NSMutableArray *notifications;
@property NSThread *notificationThread;
@property NSLock *notificationLock;
@property NSMachPort *notificationPort;
- (void)setUpThreadingSupport;
- (void)handleMachMessage:(void *)msg;
- (void)processNotification:(NSNotification *)notification;
- (void)test;
@end
@implementation A
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)test {
    [self setUpThreadingSupport];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"111" object:nil];
}
- (void)setUpThreadingSupport {
    if (self.notifications) {
        return;
    }
    self.notifications      = [[NSMutableArray alloc] init];
    self.notificationLock   = [[NSLock alloc] init];
    self.notificationThread = [NSThread mainThread];
    self.notificationPort = [[NSMachPort alloc] init];
    [self.notificationPort setDelegate:self];
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort
                                forMode:(NSString *)kCFRunLoopCommonModes];
}
- (void)handleMachMessage:(void *)msg {
    [self.notificationLock lock];
    while ([self.notifications count]) {
        NSNotification *notification = [self.notifications objectAtIndex:0];
        [self.notifications removeObjectAtIndex:0];
        [self.notificationLock unlock];
        [self processNotification:notification];
        [self.notificationLock lock];
    };
    [self.notificationLock unlock];
}
- (void)processNotification:(NSNotification *)notification {
    if ([NSThread currentThread] != self.notificationThread) {
        [self.notificationLock lock];
        [self.notifications addObject:notification];
        [self.notificationLock unlock];
        [self.notificationPort sendBeforeDate:[NSDate date]
                                   components:nil
                                         from:nil
                                     reserved:0];
    } else {
        NSLog(@"current thread %@ 刷新UI", [NSThread currentThread]);
        // 刷新UI ...
    }
}
@end

// 输出
2017-02-27 11:49:04.296 notification[29036:12827315] current thread <NSThread: 0x7be3e000>{number = 3, name = (null)}
2017-02-27 11:49:04.307 notification[29036:12827268] current thread <NSThread: 0x7bf3b970>{number = 1, name = main} 刷新UI
复制代码

三.问题

1.observer 的对象存储在何处?

// NSNotificationCenter的init方法
- (id) init {
  if ((self = [super init]) != nil) {
      _table = newNCTable();
    }
  return self;
}
复制代码

每个 NSNotificationCenter 都有一个默认的 _table。其对 observer 进行引用(iOS9以前 unsafe_unretained,iOS9以后 weak)。

2.post 的时候根据何种方式查找接受通知的对象?

仔细看一看【核心源码- Selector 形式
table 中查找 observer object 的时候,首先根据的是 object,接下来根据的是 name,可见 name 的优先级比较

3.系统的通知 Name 有哪些?

// 当程序被推送到后台时
UIKIT_EXTERN NSNotificationName const UIApplicationDidEnterBackgroundNotification       NS_AVAILABLE_IOS(4_0);
// 当程序从后台将要重新回到前台时
UIKIT_EXTERN NSNotificationName const UIApplicationWillEnterForegroundNotification      NS_AVAILABLE_IOS(4_0);
// 当程序完成载入后通知
UIKIT_EXTERN NSNotificationName const UIApplicationDidFinishLaunchingNotification;
// 应用程序转为激活状态时
UIKIT_EXTERN NSNotificationName const UIApplicationDidBecomeActiveNotification;
// 用户按下主屏幕按钮调用通知,并未进入后台状态
UIKIT_EXTERN NSNotificationName const UIApplicationWillResignActiveNotification;
// 内存较低时通知
UIKIT_EXTERN NSNotificationName const UIApplicationDidReceiveMemoryWarningNotification;
// 当程序将要退出时通知
UIKIT_EXTERN NSNotificationName const UIApplicationWillTerminateNotification;
// 当系统时间发生改变时通知
UIKIT_EXTERN NSNotificationName const UIApplicationSignificantTimeChangeNotification;
// 当StatusBar框方向将要变化时通知
UIKIT_EXTERN NSNotificationName const UIApplicationWillChangeStatusBarOrientationNotification __TVOS_PROHIBITED; // userInfo contains NSNumber with new orientation
// 当StatusBar框方向改变时通知
UIKIT_EXTERN NSNotificationName const UIApplicationDidChangeStatusBarOrientationNotification __TVOS_PROHIBITED;  // userInfo contains NSNumber with old orientation
// 当StatusBar框Frame将要改变时通知
UIKIT_EXTERN NSNotificationName const UIApplicationWillChangeStatusBarFrameNotification __TVOS_PROHIBITED;       // userInfo contains NSValue with new frame
// 当StatusBar框Frame改变时通知
UIKIT_EXTERN NSNotificationName const UIApplicationDidChangeStatusBarFrameNotification __TVOS_PROHIBITED;        // userInfo contains NSValue with old frame
// 后台下载状态发生改变时通知(iOS7.0以后可用)
UIKIT_EXTERN NSNotificationName const UIApplicationBackgroundRefreshStatusDidChangeNotification NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;
// 受保护的文件当前变为不可用时通知
UIKIT_EXTERN NSNotificationName const UIApplicationProtectedDataWillBecomeUnavailable    NS_AVAILABLE_IOS(4_0);
// 受保护的文件当前变为可用时通知
UIKIT_EXTERN NSNotificationName const UIApplicationProtectedDataDidBecomeAvailable       NS_AVAILABLE_IOS(4_0);
// 截屏通知(iOS7.0以后可用)
UIKIT_EXTERN NSNotificationName const UIApplicationUserDidTakeScreenshotNotification NS_AVAILABLE_IOS(7_0);
复制代码

四.面试题

1.通知的实现原理(结构设计、通知如何存储的、name&observer&SEL之间的关系等)

通知中心结构大概分为如下几个类

  • NSNotification 通知的模型 name、object、userinfo.
  • NSNotificationCenter通知中心 负责发送NSNotification
  • NSNotificationQueue通知队列 负责在某些时机触发 调用NSNotificationCenter通知中心 post通知

通知是结构体通过双向链表进行数据存储

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
  Observation       *wildcard;  /* 链表结构,保存既没有name也没有object的通知 */
  GSIMapTable       nameless;   /* 存储没有name但是有object的通知 */
  GSIMapTable       named;      /* 存储带有name的通知,不管有没有object  */
    ...
} NCTable;

// Observation 存储观察者和响应结构体,基本的存储单元
typedef struct  Obs {
  id        observer;   /* 观察者,接收通知的对象  */
  SEL       selector;   /* 响应方法     */
  struct Obs    *next;      /* Next item in linked list.    */
  ...
} Observation;
复制代码

主要是以key value的形式存储,这里需要重点强调一下 通知以 nameobject两个纬度来存储相关通知内容,也就是我们添加通知的时候传入的两个不同的方法.

name&observer&SEL之间的关系
name作为keyobserver作为观察者对象,当合适时机触发就会调用observerSEL

2.通知的发送时同步的,还是异步的

同步发送,因为要调用消息转发.
所谓异步,指的是非实时发送而是在合适的时机发送,并没有开启异步线程.

3.NSNotificationCenter 接受消息和发送消息是在一个线程里吗?如何异步发送消息

是的, 异步线程发送通知则响应函数也是在异步线程.
异步发送通知可以开启异步线程发送即可.

4.NSNotificationQueue是异步还是同步发送?在哪个线程响应?

// 表示通知的发送时机
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空闲时发送通知
    NSPostASAP = 2, // 尽快发送,这种时机是穿插在每次事件完成期间来做的
    NSPostNow = 3 // 立刻发送或者合并通知完成之后发送
};
复制代码
. NSPostWhenIdle NSPostASAP NSPostNow
NSPostingStyle 异步发送 异步发送 同步发送

NSNotificationCenter都是同步发送的,而这里介绍关于NSNotificationQueue的异步发送,从线程的角度看并不是真正的异步发送,或可称为延时发送,它是利用了runloop的时机来触发的.

异步线程发送通知则响应函数也是在异步线程,主线程发送则在主线程.

5.NSNotificationQueue和runloop的关系

NSNotificationQueue依赖runloop. 因为通知队列要在runloop回调的某个时机调用通知中心发送通知.从下面的枚举值就能看出来

// 表示通知的发送时机
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空闲时发送通知
    NSPostASAP = 2, // 尽快发送,这种时机是穿插在每次事件完成期间来做的
    NSPostNow = 3 // 立刻发送或者合并通知完成之后发送
};
复制代码

6.如何保证通知接收的线程在主线程

如果想在主线程响应异步通知的话可以用如下两种方式

  1. 系统接受通知的API指定队列
  2. NSMachPort的方式 通过在主线程的 runloop 中添加 machPort,设置这个 portdelegate,通过这个 Port 其他线程可以跟主线程通信,在这个 port 的代理回调中执行的代码肯定在主线程中运行,所以,在这里调用 NSNotificationCenter 发送通知即可

7.页面销毁时不移除通知会崩溃吗?

iOS9.0之前,会crash,原因:通知中心对观察者的引用是 unsafe_unretained,导致当观察者释放的时候,观察者的指针值并不为nil,出现野指针.

iOS9.0之后,不会crash,原因:通知中心对观察者的引用是 weak

8.多次添加同一个通知会是什么结果?多次移除通知呢

多次添加同一个通知,会导致发送一次这个通知的时候,响应多次通知回调。多次移除通知不会产生crash。

9.下面的方式能接收到通知吗?为什么

// 发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
复制代码

不能

通知中心存储通知观察者的结构

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
  Observation  *wildcard;    /* 链表结构,保存既没有name也没有object的通知 */
  GSIMapTable nameless;    /* 存储没有name但是有object的通知    */
  GSIMapTable named;        /* 存储带有name的通知,不管有没有object    */
    ...
} NCTable;
复制代码

当添加通知监听的时候,我们传入了nameobject,所以,观察者的存储链表是这样的:
named表:key(name) : value->key(object) : value(Observation)

因此在发送通知的时候,如果只传入name而并没有传入object,是找不到Observation的,也就不能执行观察者回调.

猜你喜欢

转载自juejin.im/post/7078958149331320839
今日推荐