底层初窥——NSNotificationCenter

介绍

消息通知在项目中使用是很频繁的,但是我们平常之事关注于使用,这篇文章我们稍微深入了解一下消息中心是怎么运作的。

通知机制的核心是一个与线程关联的单例对象叫通知中心(NSNotificationCenter)。通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。

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

官方文档地址

数据结构

这里我们的数据结构及流程分析,都是基于 GNUStep的源码

NSNotificationCenter:消息中心

1
2
3
4
5
6
typedef struct NCTbl {
Observation *wildcard; /* Get ALL messages. */
GSIMapTable nameless; /* Get messages for any name. */
GSIMapTable named; /* Getting named messages only. */
...
} NCTable;
  • wildcard
    • wildcard是链表的数据结构,如果在注册观察者时既没有传入NotificationName,也没有传入object,就会添加到wildcard的链表中。注册到这里的观察者能接收到 所有的系统通知。
  • nameless
    • 添加观察者时没有传入 NoficationName 的表
  • named
    • 添加观察者时传入了 NotificationName 的表

named

在 named 表中,NotifcationName 作为表的 key,因为我们在注册观察者的时候是可以传入一个参数 object 用于只监听指定该对象发出的通知,并且一个通知可以添加多个观察者,所以还需要一张表来保存 object 和 Observer 的对应关系。这张表的是 key、Value 分别是以 object 为 Key,Observer 为 value。用了链表这种数据结构实现保存多个观察者的情况。

在实际开发过程中 object 参数我们经常传 nil,这时候系统会根据 nil 自动生成一个 key,相当于这个 key 对应的 value(链表)保存的就是当前通知传入了 NotificationName 没有传入 object 的所有观察者。

nameless

nameless 表,较 named 表就简单了许多。因为少了 NSNotificationName 作为 Key 值,所以少了一层嵌套。

wildcard

在注册观察者时既没有传入 NSNotificationName,也没有传入 object,就会添加到 wildcard 的链表中。注册到这里的观察者能接收到所有的系统通知

Observation:保存了观察者信息

1
2
3
4
5
6
7
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;

工作流程

  • 添加观察者流程

    1. 首先会根据传入的参数实例化一个 Observation,Observation 对象保存了观察者对象,接收到通知观察者所执行的方法,以及下一个 Observation 对象的地址。
    2. 根据是否传入 NotificationName 选择操作 Named Table 还是 Nameless Table。
    3. 若传入了 NotificationName,则会以 NotificationName 为 key 去查找对应的 Value,若找到 value,则取出对应的 value;若未找到对应的 value,则新建一个 table,然后将这个 table 以 NotificationName 为 key 添加到 Named Table 中。
    4. 若在保存 Observation 的 table 中,以 object 为 key 取对应的链表。若找到了则直接在链接末尾插入之前实例化好的 Observation;若未找到则以之前实例化好的 Observation 对象作为头节点插入进去。
  • 发送通知流程

    1. 首先会创建一个数组 observerArray 用来保存需要通知的 observer。
    2. 遍历 wildcard 链表,将 observer 添加到 observerArray 数组中。
    3. 若存在 object,在 nameless table 中找到以 object 为 key 的链表,然后遍历找到的链表,将 observer 添加到 observerArray 数组中。
    4. 若存在 NotificationName,在 named table 中以 NotificationName 为 key 找到对应的 table,然后再在找到的 table 中以 object 为 key 找到对应的链表,遍历链表,将 observer 添加到 observerArray 数组中。如果 object 不 为nil,则以 nil 为 key 找到对应的链表,遍历链表,将 observer 添加到 observerArray 数组中。
    5. 至此所有关于当前通知的 observer(wildcard+nameless+named)都已经加入到了数组 observerArray 中。遍历 observerArray 数组,取出其中 的observer 节点(包含了观察者对象和 selector),调用形式如下:
    1
    [o->observer performSelector: o->selector withObject: notification];
  • 移除通知流程

    1. 若 NotificationName 和 object 都为 nil,则清空 wildcard 链表。
    2. 若 NotificationName 为 nil,遍历 named table,若 object 为 nil,则清空 named table,若 object 不为 nil,则以 object 为 key 找到对应的链表,然后清空链表。在 nameless table 中以 object 为 key 找到对应的 observer 链表,然后清空,若 object 也为 nil,则清空 nameless table。
    3. 若 NotificationName 不为nil,在 named table 中以 NotificationName 为 key 找到对应的 table,若 object 为 nil,则清空找到的 table,若 object 不为 nil,则以 object 为 key 在找到的 table 中取出对应的链表,然后清空链表。

一些问题

这一节收集了几个常见的 NSNotificationCenter 相关的问题。

通知的发送时同步的,还是异步的?发送消息与接收消息的线程是同一个线程么?

通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。

在抛出通知以后,观察者在通知事件处理完成以后(可以通过休眠3秒来测试),抛出者才会往下继续执行,也就是说这个过程默认是同步的;当发送通知时,通知中心会一直等待所有的 observer 都收到并且处理了通知才会返回到 poster。

接收通知的线程,和发送通知所处的线程是同一个线程。也就是说如果如果要在接收通知的时候更新 UI,需要注意发送通知的线程是否为主线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 异步执行 + 串行队列
NSLog(@"--current thread: %@", [NSThread currentThread]);
NSLog(@"Begin post notification");
[[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];
NSLog(@"End");
});


}

- (void)test {
NSLog(@"--current thread: %@", [NSThread currentThread]);
NSLog(@"Handle notification and sleep 3s");
sleep(3);
}

---- 输出结果 ----
13:49:05.364262+0800 --current thread: <NSThread: 0x600000960d00>{number = 3, name = (null)}
13:49:05.364445+0800 Begin post notification
13:49:05.364621+0800 --current thread: <NSThread: 0x600000960d00>{number = 3, name = (null)}
13:49:05.364750+0800 Handle notification and sleep 3s
13:49:08.370804+0800 End

如何异步发送消息?

  1. 让通知事件处理方法在子线程中执行.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"--current thread: %@", [NSThread currentThread]);
NSLog(@"Begin post notification");
[[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];
NSLog(@"End");
}

- (void)test {
dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 异步执行 + 串行队列
NSLog(@"--current thread: %@", [NSThread currentThread]);
NSLog(@"Handle notification and sleep 3s");
sleep(3);
});
}

---- 输出结果 ----
13:54:25.336948+0800 --current thread: <NSThread: 0x6000007e4b00>{number = 1, name = main}
13:54:25.337177+0800 Begin post notification
13:54:25.337360+0800 End
13:54:25.337391+0800 --current thread: <NSThread: 0x60000079a940>{number = 6, name = (null)}
13:54:25.337492+0800 Handle notification and sleep 3s

  1. 可以通过 NSNotificationQueue 的 enqueueNotification: postingStyle: 和 enqueueNotification: postingStyle: coalesceMask: forModes: 方法将通告放入队列,实现异步发送,在把通告放入队列之后,这些方法会立即将控制权返回给调用对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"--current thread: %@", [NSThread currentThread]);
NSLog(@"Begin post notification");
NSNotification *notification = [NSNotification notificationWithName:@"NotificationName"
object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
NSLog(@"End");
}

- (void)test {
NSLog(@"--current thread: %@", [NSThread currentThread]);
NSLog(@"Handle notification and sleep 3s");
sleep(3);
}

---- 输出结果 ----
13:57:18.686405+0800 --current thread: <NSThread: 0x600001e88780>{number = 1, name = main}
13:57:18.686588+0800 Begin post notification
13:57:18.686762+0800 End
13:57:18.687007+0800 --current thread: <NSThread: 0x600001e88780>{number = 1, name = main}
13:57:18.687116+0800 Handle notification and sleep 3s

NSNotificationQueue 和 runloop 的关系?

postringStyle 参数就是定义通知调用和 runloop 状态之间关系。
该参数的三个可选参数:

  1. NSPostWhenIdle:通知回调方法是等待到当下线程 runloop 进入等待状态才会调用。
  2. NSPostASAP:通知回调方法是等待到当下线程 runloop 开始接收事件源的时候就会调用。
  3. NSPostNow:其实和直接用默认的通知中心添加通知是一样的,通知马上调用回调方法。

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

官方示例-在指定线程处理通知消息

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

在观察者对象释放之前,需要调用 ==removeOberver== 方法将观察者从通知中心移除,否则程序可能会出现崩溃。但从 iOS9 开始,即使不移除观察者对象,程序也不会出现异常。

If your app targets iOS 9.0 and later or macOS 10.11 and later, you don’t need to unregister an observer in its dealloc method.

这是因为在 iOS9 以后,通知中心持有的观察者由 ==unsafe_unretained== 引用变为 ==weak== 引用。即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空。但是通过 addObserverForName:object: queue:usingBlock: 方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。

1
2
3
- (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.

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

  • 多次添加同一个通知,会导致观察者方法被执行多次。

  • 多次移除通知,没有关系。

  • 移除通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 添加
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"aa" object:nil];

// 移除
[[NSNotificationCenter defaultCenter] removeObserver:self];

/// Block
// 添加
_noty = [[NSNotificationCenter defaultCenter] addObserverForName:URLookExpandCloth object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
// 执行你想做的事情
}];

// 移除
[[NSNotificationCenter defaultCenter] removeObserver:_noty];

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

1
2
3
4
// 添加观察
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 通知发送
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

不会,上文介绍 NSNotificationCenter 时介绍了 center 的结构。

  • 注册通知在添加observer时,路径为 TestNotification -> @1 -> self
  • 发送通知在查找observer时,路径为 TestNotification -> nil -> observer list