回答-阿里、字节:一套高效的iOS面试题③(NSNotification)

412 阅读12分钟

NSNotification相关

苹果并没有开源相关代码,但是可以读下 GNUStep 的源码,基本上实现方式很具有参考性!

通知如何触发?

NSNotificationCenter提供了一个中心化的仓库,在一个程序任何一部分的改变都能通知和被通知到程序的其他部分,Observers去注册到通知中心去响应指定的事件的action,每次事件触发,通知会通过调度表,去通知监听某个事件发生的observers。

NSNotificationCenter provides a centralized hub through which any part of an application may notify and be notified of changes from any other part of the application. Observers register with a notification center to respond to particular events with a specified action. Each time an event occurs, the notification goes through its dispatch table, and messages any registered observers for that event.

如何监听全部通知?

如果想要监听所有的通知,name和object都设置为nil就可以了
if both name and object are nil, then all notifications posted will trigger.
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserverForName:nil
                    object:nil
                     queue:nil
                usingBlock:^(NSNotification *notification)
{
     NSLog(@"%@", notification.name);
}];

KVO和通知的区别?

KVO是对keypath添加观察者,NSNotificationCenter是对通知添加观察者。

Key-Value Observing adds observers for keypaths, while NSNotificationCenter adds observers for notifications. Keep this distinction clear in your mind, and proceed to use both APIs confidently.

 *  A rough picture of _GSIMapTable 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
   
   
 struct	_GSIMapNode {
     GSIMapNode	nextInBucket;	/* Linked list of bucket.	*/
     GSIMapKey	key;
     #if	GSI_MAP_HAS_VALUE //If defined as 0, then this becomes a hash table rather than
 *		a map table.
     GSIMapVal	value;
     #endif
};


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;

typedef struct NCTbl {
  Observation		*wildcard;	/* Get ALL messages.		*/
  GSIMapTable		nameless;	/* Get messages for any name.	*/
  GSIMapTable		named;		/* Getting named messages only.	*/
  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 *o;
GSIMapTable m;
GSIMapNode  n;
o = obsNew(TABLE, selector, observer); 
  if (name)
    {
      n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name); //从GSIMapTable中根据name查找GSIMapNode
      if (n == 0) //GSIMapTable没找到名为name的node
  {
    m = mapNew(TABLE); //创建一个GSIMapTable
    name = [name copyWithZone: NSDefaultMallocZone()]; //对name进行copy
    GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);//往GSIMapTable添加一个node节点,设置name
    GS_CONSUMED(name)
  }
      else
  {
    m = (GSIMapTable)n->value.ptr; //GSIMapNode 中存储着GSIMapTable
  }
      n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);//把object作为key,取出GSIMapNode
      if (n == 0)
  {
    o->next = ENDOBS;
    GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o); //把object作为key,Observation作为value,存入map中
  }
      else
  { //往链表中插入一个Observation
    list = (Observation*)n->value.ptr;
    o->next = list->next; 
    list->next = o;
  }
}

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

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

// 根容器,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同时有object的通知数据结构图:
 /*
   * Find the observers of NAME, except those observers with a non-nil OBJECT
   * that doesn't match the notification's OBJECT).
   */
  if (name)
    {
      n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
      if (n)
	{
	  m = (GSIMapTable)n->value.ptr;
	}
      else
	{
	  m = 0;
	}
      if (m != 0)
	{
	  /*
	   * First, observers with a matching object.
	   */
	  n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
	  if (n != 0)
	    {
	      o = purgeCollectedFromMapNode(m, n);
	      while (o != ENDOBS)
		{
		  GSIArrayAddItem(a, (GSIArrayItem)o);
		  o = o->next;
		}
	    }

	  if (object != nil)
	    {
	      /*
	       * Now observers with a nil object.
	       */
	      n = GSIMapNodeForSimpleKey(m, (GSIMapKey)(id)nil);
	      if (n != 0)
		{
	          o = purgeCollectedFromMapNode(m, n);
		  while (o != ENDOBS)
		    {
		      GSIArrayAddItem(a, (GSIArrayItem)o);
		      o = o->next;
		    }
		}
	    }
	}
    }

1、注册通知,如果通知的name存在,则以name为key从named字典中取出值n(这个n其实被MapNode包装了一层,便于理解这里直接认为没有包装),这个n还是个字典,各种判空新建逻辑不讨论

2、然后以object为key,从字典n中取出对应的值,这个值就是Observation类型(后面简称obs)的链表,然后把刚开始创建的obs对象o存储进去。

named.png

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

  1. 找到NCTable中的named表,这个表存储了还有name的通知
  2. name作为key,找到value,这个value依然是一个map
  3. map的结构是以object作为key,obs对象为value,这个obs对象的结构上面已经解释,主要存储了observer & SEL
注册无name的通知(只存在object)数据结构图:
 /*
   * Find the observers that specified OBJECT, but didn't specify NAME.
   */
  if (object)
    {
      n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
      if (n != 0)
	{
	  o = purgeCollectedFromMapNode(NAMELESS, n);
	  while (o != ENDOBS)
	    {
	      GSIArrayAddItem(a, (GSIArrayItem)o);
	      o = o->next;
	    }
	}
    }
  1. object为key,从nameless字典中取出value,此value是个obs类型的链表

  2. 把创建的obs类型的对象o存储到链表中

nameless.png

只存在object时存储只有一层,那就是objectobs对象之间的映射

既没有name也没有object
Observation		*wildcard;
#define	WILDCARD	(TABLE->wildcard)

o->next = WILDCARD;
WILDCARD = o;

/*
   * Find all the observers that specified neither NAME nor OBJECT.
   */
for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next)
  {
    GSIArrayAddItem(a, (GSIArrayItem)o);
  }

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

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

// 发送通知
- (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执行响应方法,从这里可以看出是同步的
    count = GSIArrayCount(a);
    while (count-- > 0)
    {
      o = GSIArrayItemAtIndex(a, count).ext;
     [o->observer performSelector: o->selector
                                withObject: notification]; 
    }
	//step3: 释放资源
    RELEASE(notification);
}

在发送通知的时候从WILDCARD、NAMELESS、NAMED存储容器里面找出传入的通知对应的Observation,

然后把这些Observation加入到数组中,遍历数组找到observer 然后调用selector,这个是同步的操作。

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

3.1、接受消息和发送消息是在一个线程里吗?

发送消息:
[o->observer performSelector: o->selector
                                withObject: notification];
接收消息:

就是从存储容器中找到对应的Observation,对存入的observer调用selector,所以接受消息和发送消息是在一个线程里。

3.2、如何异步发送消息

    //step2:执行发送,即调用performSelector执行响应方法,从这里可以看出是同步的
    count = GSIArrayCount(a);
    while (count-- > 0)
    {
      o = GSIArrayItemAtIndex(a, count).ext;
     [o->observer performSelector: o->selector
                                withObject: notification]; 
    }

可以在上面的while循环里面,开启一个异步线程进行发送。

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

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

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

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

5、NSNotificationQueue和runloop的关系?

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

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

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

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

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

  1. 使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block
  2. 在主线程注册一个machPort,它是用来做线程通信的,当在异步线程收到通知,然后给machPort发送消息,这样肯定是在主线程处理的,具体用法去网上资料很多,苹果官网也有
//
//  SecondViewController.m
//  NotificationWithSubThread
//
//  Created by Mr.LuDashi on 2017/8/22.
//  Copyright © 2017年 ZeluLi. All rights reserved.
//

#import "SecondViewController.h"

@interface SecondViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSMutableArray    *notificationsQueue;    //存储子线程发出的通知的队列
@property (nonatomic, strong) NSThread          *mainThread;            // 处理通知事件的预期线程
@property (nonatomic, strong) NSLock            *lock;                  // 用于对通知队列加锁的锁对象,避免线程冲突
@property (nonatomic, strong) NSMachPort        *machPort;              // 用于向期望线程发送信号的通信端口
@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    //打印注册观察者的线程,此处也就是主线程
    NSLog(@"register notificaton thread = %@", [NSThread currentThread]);
   
    [self setUpThreadingSupport]; // 对相关的成员属性进行初始
    
    [[NSNotificationCenter defaultCenter]
     addObserver:self
     selector:@selector(processNotification:)
     name:@"NotificationName"
     object:nil];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"post notificaton thread = %@", [NSThread currentThread]);
        [[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil userInfo:nil];
    });
}


/**
 对相关的成员属性进行初始
 */
- (void) setUpThreadingSupport {
    if (self.notificationsQueue) {
        return;
    }
    self.notificationsQueue = [[NSMutableArray alloc] init];    //队列:用来暂存其他线程发出的通知
    self.lock = [[NSLock alloc] init];                          //负责栈操作的原子性
    self.mainThread = [NSThread currentThread];                 //记录处理通知的线程
    self.machPort = [[NSMachPort alloc] init];                  //负责往处理通知的线程所对应的RunLoop中发送消息的
    [self.machPort setDelegate:self];
    
    [[NSRunLoop currentRunLoop] addPort:self.machPort           //将Mach Port添加到处理通知的线程中的RunLoop中
                                forMode:(__bridge NSString *)kCFRunLoopCommonModes];
}


/**
 从子线程收到Mach Port发出的消息后所执行的方法
 在该方法中从队列中获取子线程中发出的NSNotification
 然后使用当前线程来处理该通知

 RunLoop收到Mach Port发出的消息时所执行的回调方法。
 */
- (void)handleMachMessage:(void *)msg {
    
    NSLog(@"handle Mach Message thread = %@", [NSThread currentThread]);
    
    //在子线程中对notificationsQueue队列操作时,需要加锁,保持队列中数据的正确性
    [self.lock lock];
    
    //依次取出队列中所暂存的Notification,然后在当前线程中处理该通知
    while ([self.notificationsQueue count]) {
        NSNotification *notification = [self.notificationsQueue objectAtIndex:0];
        
        [self.notificationsQueue removeObjectAtIndex:0]; //取出队列中第一个值
        
        [self.lock unlock];
        [self processNotification:notification];    //处理从队列中取出的通知
        [self.lock lock];
        
    };
    
    [self.lock unlock];
}


- (void)processNotification:(NSNotification *)notification {
    
    if ([NSThread currentThread] == _mainThread) {
        //处理出队列中的通知
        NSLog(@"handle notification thread = %@", [NSThread currentThread]);
        
    } else { //在子线程中收到通知后,将收到的通知放入到队列中存储,然后给主线程的RunLoop发送处理通知的消息
        NSLog(@"transfer notification thread = %@", [NSThread currentThread]);
        
        // Forward the notification to the correct thread.
        [self.lock lock];
        [self.notificationsQueue addObject:notification];    //将其他线程中发过来的通知不做处理,入队列暂存
        [self.lock unlock];
        
        //通过MachPort给处理通知的线程发送通知,使其处理队列中所暂存的队列
        [self.machPort sendBeforeDate:[NSDate date]
                           components:nil
                                 from:nil
                             reserved:0];

    }
}

@end

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

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

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

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

添加通知
- (void) addObserver: (id)observer
	    selector: (SEL)selector
                name: (NSString*)name
	      object: (id)object
{
  Observation	*list;
  Observation	*o;
  GSIMapTable	m;
  GSIMapNode	n;
/*
       * Add the observation to the list for the correct object.
       */
      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;
	}
}

可以从上面的代码看到,多次注册同一个通知,会不停的往链表追加Observation。

发送通知
- (void) postNotification: (NSNotification*)notification
{
  [self _postAndRelease: RETAIN(notification)];
}

- (void) _postAndRelease: (NSNotification*)notification
{
  Observation	*o;
  unsigned	count;
  NSString	*name = [notification name];
  id		object;
  GSIMapNode	n;
  GSIMapTable	m;
  GSIArrayItem	i[64];
  GSIArray_t	b;
  GSIArray	a = &b;
  
1、无object无name
  for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next)
    {
      GSIArrayAddItem(a, (GSIArrayItem)o);
    }

1、有object无name
  /*
   * Find the observers that specified OBJECT, but didn't specify NAME.
   */
  if (object)
    {
      n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
      if (n != 0)
	{
	  o = purgeCollectedFromMapNode(NAMELESS, n);
	  while (o != ENDOBS)
	    {
	      GSIArrayAddItem(a, (GSIArrayItem)o);
	      o = o->next;
	    }
	}
    }

3、有name也有object
	  /*
	   * First, observers with a matching object.
	   */
	  n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
	  if (n != 0)
	    {
	      o = purgeCollectedFromMapNode(m, n);
	      while (o != ENDOBS)
		{
		  GSIArrayAddItem(a, (GSIArrayItem)o);
		  o = o->next;
		}
	    }
	    }

从上面的代码可以看出,发送通知时,会遍历存储结构的Observation,添加到数组中,逐个进行调用。

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

多次移除通知呢?
/**
 * Removes the item for the specified key from the map.
 * If the key was present, returns YES, otherwise returns NO.
 */
GS_STATIC_INLINE BOOL
GSIMapRemoveKey(GSIMapTable map, GSIMapKey key)
{
  GSIMapBucket	bucket = GSIMapBucketForKey(map, key);
  GSIMapNode	node;
  
  node = GSIMapNodeForKeyInBucket(map, bucket, key);
  if (node != 0)
    {
      GSIMapRemoveNodeFromMap(map, bucket, node);
      GSIMapFreeNode(map, node);
      return YES;
    }
  return NO;
}

根据name找到节点,就把节点从map中移除,找不到不做处理,所以多次移除通知不会产生crash。

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

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

不能。

当添加通知监听的时候,我们传入了nameobject,所以,观察者的存储链表是这样的:有name也有object的底层存储数据结构是下面这样。

named.png

有name无object 底层存储数据结构是下面这样。

nameless.png

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

参考资料:

一套高效的iOS面试题之NSNotification相关

轻松过面:一文全解iOS通知机制