关于NSNotification一直停留在对其的基本使用层面,了解其是观察者模式的架构设计,但是内部实现原理并没有了解过。直到最近刷到了一些关于NSNotification的面试题,于是便有了这篇文章。
NSNotification原理
有非常多的文章讲述了关于如何使用NSNotification,这里就不再叙述。我们直入NSNotification是怎么实现。
数据结构
由于苹果没有对相关源码开放,我们先来看看苹果开放给我们的api和类。
NSNotification
我们先来看NSNotification类的数据结构,通过.h文件我们可以看到,他包含下面属性。
@interface NSNotification : NSObject <NSCopying, NSCoding>
...
/* Querying a Notification Object */
- (NSString*) name; // 通知的name
- (id) object; // 携带的对象
- (NSDictionary*) userInfo; // 配置信息
@end
NSNotificationCenter
再来看我们使用最多的NSNotificationCenter,他是个单例类,主要负责三件事:
- 添加通知
- 发送通知
- 移除通知 核心API如下:
// 添加通知观察者
- (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;
通过苹果开放给我们的API,我们可以对其实现做下猜测,苹果应该维护了NSNotificationName、observer、selector和object的映射关系。具体是怎么实现的呢?
我们再以GNUStep源码为基础进行研究,GNUStep虽然不是苹果官方的源码,但很具有参考意义,通过源码我们了解到NSNotificationCenter中主要定义了两个table,同时也定义了Observation保存观察者信息。
Observation
先来看Observation结构体:
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;
- 保存了observer和selector信息
- 链表结构,指向注册了同一个通知的下一个观察者
NCTable
再来看内部保存的两张表,一张用于保存添加观察者的时候传入了NotifcationName的情况。一张用于保存添加观察者的时候没有传入了NotifcationName的情况,下面分两种情况分析。
typedef struct NCTbl {
Observation *wildcard; /* Get ALL messages. */
GSIMapTable nameless; /* Get messages for any name. */
GSIMapTable named; /* Getting named messages only. */
} NCTable;
Named Table
先看用于保存添加观察者的时候传入了NotifcationName的表Named Table。根据表的名字,我们可以猜测出,在Named Table中,NotifcationName作为表的key。但是根据苹果给出的API,我们在注册观察者的时候是可以传入一个参数object用于只监听指定该对象发出的通知,所以还需要一张表来保存object和Observer的对应关系。这张表分别是以object为Key,Observer为value。
- 外层是一个table,以通知名称NotificationName为key,其value为一个table(后面简称为内层table)。
- 内层table以object为key,其value为一个链表,用来保存所有的观察者。
- object为nil时系统会根据nil自动生成一个key,相当于这个key对应的value(链表)保存的就是当前通知传入了NotificationName没有传入object的所有观察者。
Nameless Table
Nameless Table比Named Table要简单很多,因为没有NotificationName作为key,直接用object作为key。相较于Named Table要少一层table嵌套。
wildcard
wildcard是链表的数据结构,如果在注册观察者时既没有传入NotificationName,也没有传入object,就会添加到wildcard的链表中。注册到这里的观察者能接收到 所有的系统通知。
添加通知
先来看源码:
/*
observer:观察者,即通知的接收者
selector:接收到通知时的响应方法
name: 通知name
object:携带对象
*/
- (void) addObserver: (id)observer
selector: (SEL)selector
name: (NSString*)name
object: (id)object {
Observation *list;
Observation *o;
GSIMapTable m;
GSIMapNode n;
// 前置条件判断
......
// 创建一个observation对象,持有观察者和SEL,下面进行的所有逻辑就是为了存储它
o = obsNew(TABLE, selector, observer);
//如果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
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;
}
}
//如果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 { // 存在 则把值接到链表的节点上
...
}
}
//name 和 object 都为空 则存储到wildcard链表中
else {
o->next = WILDCARD;
WILDCARD = o;
}
}
- 实例化Observation保存了观察者对象observer、接收到通知时所执行的方法selector
- 如果传入通知的Name存在,则在Named Table中以Name为Key,去查找是否有对应value,如果没有value,在创建一个新的table,如果有则取出这个内层table
- 取出保存了object和Observation关系的内层table,则根据object去查找对应的Observation,如果没有object则会有一个默认的key,表示所有任何地方发送通知都会监听
- 通过object生成的key查找Observation链表,如果没有则创建一个节点,并作为头节点。如果有链表,则插入到尾部
- 如果传入通知的Name不存在,但是object不为空。则通过object生成的key查找Observation链表,如果没有则创建一个节点,并作为头节点。如果有链表,则插入到尾部
- 如果Name和object都为空,直接把Observation对象存放在了wildcard链表结构中
发送通知
发送通知主要是调用了_postAndRelease方法,我们直接看该方法的实现可以简化为:
- (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;
// 前置条件判断
......
object = [notification object];
// 创建一个Array来保存Observation
GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);
// 将wildcard中所有Observation都保存到Array,这些Observation是即没有name也没有object
for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next)
{
GSIArrayAddItem(a, (GSIArrayItem)o);
}
// 在nameless Table中,查找定义了对应object的Observation,添加到Array
if (object)
{
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
if (n != 0)
{
o = purgeCollectedFromMapNode(NAMELESS, n);
while (o != ENDOBS)
{
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
}
/*
* 在named table中查找,先以name为key,查找对应的value值即内层table
* 在内层table里,先根据object查找对应的Observation,依次添加到Array中
* 如果object不为nil,再以nil为object key查找对应的Observation,依次添加到Array中
*/
// 在named table中查找,先
if (name)
{
n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
if (n)
{
m = (GSIMapTable)n->value.ptr;
}
else
{
m = 0;
}
if (m != 0)
{
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n != 0)
{
o = purgeCollectedFromMapNode(m, n);
while (o != ENDOBS)
{
//根据object查找对应的Observation,依次添加到Array中
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
if (object != nil)
{
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);
if (n != 0)
{
o = purgeCollectedFromMapNode(m, n);
while (o != ENDOBS)
{
// 以nil为object key查找对应的Observation,依次添加到Array中
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
}
}
}
// 遍历Array,逐一调用observer的selector方法
count = GSIArrayCount(a);
while (count-- > 0) {
o = GSIArrayItemAtIndex(a, count).ext;
if (o->next != 0) {
[o->observer performSelector: o->selector
withObject: notification];
}
}
// 释放notification对象
RELEASE(notification);
}
- 根据name、object、info生成notification对象
- 创建一个Array来保存Observation信息
- 将wildcard中所有Observation都保存到Array,这些Observation是即没有name也没有object
- 在nameless Table中,查找定义了对应object的Observation,添加到Array
- 在named table中查找,先以name为key,查找对应的value值即内层table
- 在内层table里,先根据object查找对应的Observation,依次添加到Array中
- 如果object不为nil,再以nil为object key查找对应的Observation,依次添加到Array中
- 遍历Array,逐一调用observer的selector方法,通过代码可以看出是在同一个线程中同步执行
- 释放notification对象
移除通知
移除通知的代码也很长,但是其原理其实可以类比添加和发送通知,还是对2个table和wildcard的操作,流程大概如下:
- name、object都为nil,则移除wildcard链表中observer相同的Observation信息
- 如果name为nil,
- 先在named table中遍历所有内层table,针对每个内层table
- 若object为nil,则遍历table中所有的value,清空observer相同的Observation信息
- 若object不为nil,则以object为key在找到的table中取出对应的链表,然后清空observer相同的Observation信息。
- 再处理nameless table
- 若object为nil,则遍历table中所有的value,清空observer相同的Observation信息
- 若object不为nil,则以object为key在找到的table中取出对应的链表,然后清空observer相同的Observation信息。
- 先在named table中遍历所有内层table,针对每个内层table
- 如果name不为nil,在named table中以name为key找到对应的内层table
- 若object为nil,则遍历table中所有的value,清空observer相同的Observation信息
- 若object不为nil,则以object为key在找到的table中取出对应的链表,然后清空observer相同的Observation信息。
待扩展
已经介绍完notification的基本原理,当然也还有很多问题没有涉及,例如NSNotificationQueue,再例如下面这个添加通知的api,有时间的小伙伴可以自己再深入研究下。
- (id) addObserverForName: (NSString *)name
object: (id)object
queue: (NSOperationQueue *)queue
usingBlock: (NSNotificationBlock)block
问题解答
回到一开始了解NSNotification内部实现原理的初衷,现在来列举下这些面试题,并给出答案
实现原理(结构设计、通知如何存储的、name&observer&SEL之间的关系等)
即上一章节阐述的内容
通知的发送时同步的,还是异步的
在发送通知的源码中,我们可以得出,不仅是同步的,而且发送和接收通知还在同一个线程。
页面销毁时不移除通知会崩溃吗
我们先来看一段系统说明:
If you used addObserverForName:object:queue:usingBlock: to create your observer, you should call
this method or removeObserver:name:object: before the system deallocates any object that
addObserverForName:object:queue:usingBlock: specifies.
If your app targets iOS 9.0 and later or macOS 10.11 and later, and you used addObserver:selector:name:object:,
you do not need to unregister the observer. If you forget or are unable to remove the observer, the system
cleans up the next time it would have posted to it.
When removing an observer, remove it with the most specific detail possible. For example, if you used a name
and object to register the observer, use removeObserver:name:object: with the name and object.
通过这段文档,我们可以看出
- 使用addObserverForName:object:queue:usingBlock,必须自己手动移除
- 使用addObserver:selector:name:object:,ios9后系统会自动移除
如何自动移除
ios9以后,系统使用weak指针修饰observe,当observe被释放后,再次发送消息给nil发送不会引起崩溃,并且根据描述中提到,系统会下次发送通知时,移除这些oboserve为nil的观察者
多次添加同一个通知会是什么结果?多次移除通知呢
由于源码中并不会进行重复过滤,所以添加同一个通知,等于就是添加了2次,回调也会触发两次。 多次移除,并没有问题,因为会去map中查找,找到才会删除。
下面的方式能接收到通知吗?为什么
// 发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
不会,根据postNotification的实现,
- 会从named table中找到key为TestNotification的子table
- 再从该table中选择key为nil的observation链表信息,依次发送通知