iOS之深入解析通知NSNotification的底层原理

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析通知NSNotification的底层原理相关的知识,希望对你有一定的参考价值。

一、概念

① NSNotification
  • NSNotification 用于描述通知的类,一个 NSNotification 对象就包含了一条通知的信息,NSNotification 对象是不可变的。所以当创建一个通知时通常包含如下属性:
	@interface NSNotification : NSObject <NSCopying, NSCoding>
	
	@property (readonly, copy) NSNotificationName name;
	@property (nullable, readonly, retain) id object;
	@property (nullable, readonly, copy) NSDictionary *userInfo;

	@end
  • NSNotification 可以通过 Designaged Initializer 函数创建实例对象:
	- (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;
  • 也可以通过NSNotification (NSNotificationCreation) 分类相应的方法创建 NSNotification 的实例对象:
	@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
  • 实际开发中更多的是直接调用 NSNoficationCenter 的 postNotificationName:object: 或 postNotificationName:object:userInfo: 方法发送通知,这两个方法内部会根据传入的参数直接创建通知对象。
② NSNoficationCenter
  • 通知机制的核心是一个与线程关联的单例对象叫通知中心 NSNotificationCenter。通知中心发送通知给观察者是同步的,也可以用通知队列 NSNotificationQueue 异步发送通知。

在这里插入图片描述

  • NSNotificationCenter 提供了一种互不相干的对象之间能够相互通信的方式,它接收 NSNotification 对象并把通知广播给所有感兴趣的对象。NSNotificationCenter 的属性只有一个 defaultCenter,而且这个属性是只读的:
	@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
  • NSNoficationCenter 是一个单例类,负责管理通知的创建和发送,属于最核心的类了。NSNotificationCenter 的方法分为三类:添加通知观察者的方法、发出通知的方法、移除通知观察者的方法:
	// 添加通知观察者
	- (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;
	
	// 发出通知
	- (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;
  • 注意:
    • 若 notificationName 为 nil,通知中心会通知所有与该通知中 object 相匹配的监听对象。
    • 若 anObject 为 nil,通知中心会通知所有与该通知中 notificationName 相匹配的监听对象。
    • iOS9 以后,NSNofitifcationCenter 无需手动移除观察者。
      • 在观察者对象释放之前,需要调用 removeOberver 方法将观察者从通知中心移除,否则程序可能会出现崩溃。
      • 在 iOS9 开始,即使不移除观察者对象,程序也不会出现异常。这是因为在 iOS9 以后,通知中心持有的观察者由 unsafe_unretained 引用变为 weak 引用,即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空。但是通过 addObserverForName:object: queue:usingBlock: 方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。
③ NSNotificationQueue
  • NSNotificationQueue 通知队列充当通知中心的缓冲区,通知队列通常以FIFO(先进先出)的顺序来维护通知。每个线程都有一个与缺省通知中心 default notification center 相关的缺省通知队列 defaultQueue。
	// 缺省的通知队列
	@property (class, readonly, strong) NSNotificationQueue *defaultQueue;
	
	// 指定初始化函数
	- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
  • 通过 defaultQueue 获取默认的通知队列或者通过指定初始化函数 initWithNotificationCenter: 创建通知队列,最终都是通过 NSNotificationCenter 来发送、注册通知。
	// 往通知队列添加通知
	- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
	- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes; // 如果modes为nil,则对于runloop的所有模式发送通知都是有效的
	
	// 移除通知队列中的通知
	- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
  • NSNotificationQueue 是依赖 runloop 的,所以如果线程的 runloop 未开启则无效。NSNotificationQueue 主要做了两件事:添加通知到队列、删除通知。核心 API 如下:
	// 把通知添加到队列中,NSPostingStyle是个枚举,下面会介绍
	- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
	// 删除通知,把满足合并条件的通知从队列中删除
	- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
  • 合并通知:
    • NSNotificationNoCoalescing 不合并队列中的通知;
    • NSNotificationCoalescingOnName 按通知名称合并通知;
    • NSNotificationCoalescingOnSender 按传入的 object 合并通知。
  • 发送方式:
    • NSPostASAP 在当前通知调用结束或计时器超时发送通知;
    • NSPostWhenIdle 当 runloop 处于空闲状态时发送通知;
    • NSPostNow 在合并通知后立即发送通知。
  • 队列的合并策略和发送时机:把通知添加到队列等待发送,同时提供了一些附加条件供开发者选择,如:什么时候发送通知、如何合并通知等,系统给了如下定义:
	// 表示通知的发送时机
 	typedef NS_ENUM(NSUInteger, NSPostingStyle) {
		NSPostWhenIdle = 1, // runloop空闲时发送通知
		NSPostASAP = 2, // 尽快发送,这种情况稍微复杂,这种时机是穿插在每次事件完成期间来做的
		NSPostNow = 3 // 立刻发送或者合并通知完成之后发送
 	};
 	// 通知合并的策略,有些时候同名通知只想存在一个,这时候就可以用到它了
 	typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
		NSNotificationNoCoalescing = 0, // 默认不合并
		NSNotificationCoalescingOnName = 1, // 只要name相同,就认为是相同通知
		NSNotificationCoalescingOnSender = 2  // object相同
	};
  • GSNotificationObserver 类是 GNUStep 源码中定义的,它的作用是代理观察者,主要用来实现接口:addObserverForName:object: queue: usingBlock: 时用到,即要实现在指定队列回调 block,那么 GSNotificationObserver 对象保存了 queue 和 block 信息,并且作为观察者注册到通知中心,等到接收通知时触发了响应方法,并在响应方法中把 block 抛到指定 queue 中执行,定义如下:
	@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

二、底层实现

① 通知结构分析
  • NSNotification 是一个类蔟不能够实例化,当调用 initWithName:object:userInfo: 方法的时候,系统内部会实例化 NSNotification 的子类 NSConcreteNotification。在这个子类中重写了 NSNofication 定义的相关字段和方法。
  • NSNotificationCenter 是通知的管理类,实现较复杂。NSNotificationCenter 中主要定义了两个 table,同时也定义了 Observation 保存观察者信息,它们结构体可以简化如下:
	typedef struct NCTbl {
	  Observation       *wildcard;  /* Get ALL messages. */
	  GSIMapTable       nameless;   /* Get messages for any name. */
	  GSIMapTable       named;      /* Getting named messages only. */
	} NCTable;
	typedef struct  Obs {
	  id        observer;   /* Object to receive message.   */
	  SEL       selector;   /* Method selector.     */
	  struct Obs    *next;      /* Next item in linked list.    */
	  int       retained;   /* Retain count for structure.  */
	  struct NCTbl  *link;      /* Pointer back to chunk table  */
	} Observation;
  • 在 NSNotificationCenter 内部保存了两张表:一张用户保存添加观察者时传入了 NotificationName 的情况,一种用户保存添加观察者时没有传入 NoficationName 的情况。
  • 在 named table 中,NotificationName 作为表的 key,但因注册观察者的时可传入一个 object 参数用于接收指定对象发出的通知,并且一个通知可注册多个观察者,所以还需要一张表来保存 object 和 observer 的对应关系,这张表以 object 为 key, observer 为 value,用链表的数据结构实现同一个通知保存多个观察者的情况。
  • named table 最终的数据结构如下图所示:
    • 外层是一个 table,以通知名称 NotificationName 为 key,其 value 为一个 table(简称内层 table)。
    • 内层 table 以 object 为 key,其 value 为一个链表,用来保存所有的观察者。

在这里插入图片描述

  • 在实际开发过程中 object 参数我们经常传 nil,这时候系统会根据 nil 自动生成一个 key,相当于这个 key 对应的 value(链表)保存的就是当前通知传入了 NotificationName 没有传入 object 的所有观察者。当对应的 NotificationName 的通知发送时,链表中所有的观察者都会收到通知。
  • Nameless Table 比 Named Table 要简单很多,因为没有 NotificationName 作为 key,直接用 object 作为 key,相较于 Named Table 要少一层 table 嵌套。Nameless Table 如下所示:

在这里插入图片描述

  • wildcard 是链表的数据结构,如果在注册观察者时既没有传入 NotificationName,也没有传入 object,就会添加到 wildcard 的链表中。注册到这里的观察者能接收到 所有的系统通知。
② 注册通知分析
  • 在初始化 NotificationCenter 时会创建一个对象,这个对象里保存了 Named Table、Nameless Table、wildcard 和一些其它信息。所有注册观察者的操作最后都会调用 addObserver:selector:name:object:。源码实现如下:
	/*
	 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 结构体中核心的三个变量以及功能:wildcard、named、nameless,在源码中直接用宏定义表示了:WILDCARD、NAMELESS、NAMED。
    • case1: 存在 name(无论 object 是否存在):
      • 注册通知,如果通知的 name 存在,则以 name 为 key 从 named 字典中取出值 n(这个 n 其实被 MapNode 包装了一层,便于理解这里直接认为没有包装),这个 n 还是个字典,各种判空新建逻辑不讨论;
      • 然后以 object 为 key,从字典 n 中取出对应的值,这个值就是 Observation 类型(后面简称 obs)的链表,然后把刚开始创建的 obs 对象 o 存储进去;

在这里插入图片描述

    • 如果注册通知时传入 name,那么会是一个双层的存储结构:
      • 找到 NCTable 中的 named 表,这个表存储了还有 name 的通知
        以 name 作为 key,找到 value,这个 value 依然是一个 map;
      • map 的结构是以 object 作为 key,obs 对象为 value,这个 obs 对象的结构上面已经解释,主要存储了observer & SEL;
    • case2: 只存在 object:
      • 以 object 为 key,从 nameless 字典中取出 value,此 value 是个 obs 类型的链表;
      • 把创建的 obs 类型的对象 o 存储到链表中;
      • 只存在 object 时存储只有一层,那就是 object 和 obs 对象之间的映射。

在这里插入图片描述

    • case3:没有 name 和 object:直接把 obs 对象存放在了 Observation *wildcard 链表结构中。
  • 流程总结:
    • 首先会根据传入的参数实例化一个 Observation,Observation 对象保存了观察者对象,接收到通知观察者所执行的方法,以及下一个 Observation 对象的地址。
    • 根据是否传入 NotificationName 选择操作 Named Table 还是 Nameless Table。
    • 若传入了 NotificationName,则会以 NotificationName 为 key 去查找对应的 Value,若找到 value,则取出对应的 value;若未找到对应的 value,则新建一个 table,然后将这个 table以NotificationName 为 key 添加到 Named Table 中。
    • 若在保存 Observation 的 table 中,以 object 为 key 取对应的链表。若找到了则直接在链接末尾插入之前实例化好的 Observation;若未找到则以之前实例化好的 Observation 对象作为头节点插入进去。
  • 没有传入 NotificationName 的情况和上面的过程类似,只不过是直接根据对应的 object 为 key 去找对应的链表而已。如果既没有传入 NotificationName 也没有传入 object,则这个观察者会添加到 wildcard 链表中。
  • addObserverForName 实现的功能是在接收到通知时,在指定队列 queue 执行 block:
	// 这个api使用频率较低,怎么实现在指定队列回调block的,值得研究
	- (id) addObserverForName: (NSString *)name 
	                   object: (id)object 
	                    queue: (NSOperationQueue *)queue 
	               usingBlock: (GSNotificationBlock)block
	{
	    // 创建一个临时观察者
	    GSNotificationObserver *observer = 
	        [[GSNotificationObserver alloc] initWithQueue: queue block: block];
	    // 调用了接口1的注册方法
	    [self addObserver: observer 
	             selector: @selector(didReceiveNotification:) 
	                 name: name 
	               object: object];
	
	    return observer;
	}
  • 源码分析:
    • 创建一个 GSNotificationObserver 类型的对象 observer,并把 queue 和 block 保存下来;
    • 调用 addObserver 进行通知的注册;
    • 接收到通知时会响应 observer 的 didReceiveNotification: 方法,然后在 didReceiveNotification: 中把 block 抛给指定的 queue 去执行。
③ 发送通知分析
  • 发送通知一般调用 postNotificationName:object:userInfo: 来实现,内部会根据传入的参数实例化一个 NSNotification 对象,包含 name、object、userinfo 等信息。发送通知的源码实现基本上就是查找和调用响应方法,核心函数如下:
	// 发送通知
	- (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执行响应方法,从这里可以看出是同步的
	    while (count-- > 0) {
	      o = GSIArrayItemAtIndex(a, count).ext;
	      if (o->next != 0) {
	          NS_DURING {
	              [o->observer performSelector: o->selector
	                                withObject: notification];
	            }
	          NS_HANDLER
	            {
		      BOOL	logged;
	
		      /* Try to report the notification along with the exception,
		       * but if there's a problem with the notification itself,
		       * we just log the exception.
		       */
		      NS_DURING
			NSLog(@"Problem posting %@: %@", notification, localException);
			logged = YES;
		      NS_HANDLER
			logged = NO;
		      NS_ENDHANDLER
	  	      if (NO == logged)
			{ 
			  NSLog(@"Problem posting notification: %@", localException);
			}  
	            }
	          NS_ENDHANDLER
		}
	    }
	  lockNCTable(TABLE);
	  GSIArrayEmpty(a);
	  unlockNCTable(TABLE);
	  // step3: 释放资源
	  RELEASE(notification);
	}
  • 源码说明:
    • 通过 namoe&bject 查找到所有的 obs 对象(保存了 observer 和 sel),放到数组中;
    • 通过 performSelector:逐一调用 sel,这是个同步操作;
    • 释放 notification 对象。
  • 流程总结:
    • 首先会创建一个数组 observerArray 用来保存需要通知的 observer。
    • 遍历 wildcard 链表,将 observer 添加到 observerArray 数组中。
    • 若存在 object,在 nameless table 中找到以 object 为 key 的链表,然后遍历找到的链表,将 observer 添加到 observerArray 数组中。
    • 若存在 NotificationName,在 named table 中以 NotificationName 为 key 找到对应的 table,然后再在找到的 table 中以 object 为 key 找到对应的链表,遍历链表,将 observer 添加到 observerArray 数组中。如果 object 不为 nil,则以 nil 为 key 找到对应的链表,遍历链表,将 observer 添加到 observerArray 数组中。
    • 至此所有关于当前通知的 observer(wildcard+nameless+named)都已经加入到了数组 observerArray 中。遍历 observerArray 数组,取出其中的 observer 节点(包含了观察者对象和 selector),调用形式如下:
	[o->observer performSelector: o->selector withObject: notification];
  • 这种处理通知的方式也就能说明,发送通知的线程和接收通知的线程是同一线程。
④ 移除通知分析
  • 若 NotificationName 和 object 都为 nil,则清空 wildcard 链表。
  • 若 NotificationName 为 nil,遍历 named table,若 object 为 nil,则清空 named table,若 object 不为 nil,则以 object 为 key 找到对应的链表,然后清空链表。在 nameless table 中以 object 为 key 找到对应的 observer 链表,然后清空,若 object 也为 nil,则清空 nameless table。
  • 若 NotificationName 不为 nil,在 named table 中以 NotificationName 为 key 找到对应的 table,若 object 为 nil,则清空找到的 table,若 object 不为 nil,则以 object 为 key 在找到的 table 中取出对应的链表,然后清空链表。
	- (void) removeObserver: (id)observer
			   name: (NSString*)name
	                 object: (id)object
	{
	  if (name == nil && object == nil && observer == nil)
	      return;
	
	  /*
	   *	NB. The removal algorithm depends on an implementation characteristic
	   *	of our map tables - while enumerating a table, it is safe to remove
	   *	the entry returned by the enumerator.
	   */
	
	  lockNCTable(TABLE);
	
	  if (name == nil && object == nil)
	    {
	      WILDCARD = listPurge(WILDCARD, observer);
	    }
	
	  if (name == nil)
	    {
	      GSIMapEnumerator_t	e0;
	      GSIMapNode		n0;
	
	      /*
	       * First try removing all named items set for this object.
	       */
	      e0 = GSIMapEnumeratorForMap(NAMED);
	      n0 = GSIMapEnumeratorNextNode(&e0);
	      while (n0 != 0)
		{
		  GSIMapTable		m = (GSIMapTable)n0->value.ptr;
		  NSString		*thisName = (NSString*)n0->key.obj;
	
		  n0 = GSIMapEnumeratorNextNode(&e0);
		  if (object == nil)
		    {
		      GSIMapEnumerator_t	e1 = GSIMapEnumeratorForMap(m);
		      GSIMapNode		n1 = GSIMapEnumeratorNextNode(&e1);
	
		      /*
		       * Nil object and nil name, so we step through all the maps
		       * keyed under the current name and remove all the objects
		       * that match the observer.
		       */
		      while IOS NSNotification Center 通知中心的使用

关于iOS开发中NSNotification

ios8调用 - (void) onKeyboardShow:(NSNotification *)通知两次

iOS NSNotification传递带参数的通知

iOS之深入解析KVO的底层原理

iOS 项目中的NSNotification简单使用