李峰峰博客

NSNotification 实现原理

2020-09-05

一、NSNotification 的基本使用

1、NSNotification

NSNotification 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface NSNotification : NSObject <NSCopying, NSCoding>

// 通知名称,通知的唯一标识
@property (readonly, copy) NSNotificationName name;
// 任意对象,通常是通知发送者。如果设置了值,注册的通知监听器的 object 需要与通知的 object 匹配,否则接收不到通知
@property (nullable, readonly, retain) id object;
// 通知的附加信息
@property (nullable, readonly, copy) NSDictionary *userInfo;

// NSNotification 指定初始化方法
- (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

可以看到 NSNotification 中有一个初始化方法,除了使用该方法进行初始化,还可以使用 NSNotification(NSNotificationCreation) 分类中定义的初始化方法:

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

NSNotification(NSNotificationCreation) 分类中对 init 方法注释提示不要用来初始化。如果使用 init 方法初始化 NSNotification 会发生如下 Crash:

不过,开发时一般不会手动创建 NSNotification,而是使用 NSNotificationCenter 创建并发送通知。

2、NSNotificationCenter

NSNotificationCenter 提供了一个 defaultCenter 属性,用于获取 NSNotificationCenter 单例,并且提供了添加观察者、发送通知、移除通知的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;

// 添加通知观察者
- (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;

发送通知的时候,可以指定 notificationNameobject

  • notificationNamenil,通知中心会通知所有与该通知中 object 相匹配的监听对象;
  • anObjectnil,通知中心会通知所有与该通知中 notificationName 相匹配的监听对象。
  • 如果 notificationNameanObject 均为 nil,监听者将会收到所有发出的通知。

可以看到,添加通知观察者除了我们常用的 addObserver:selector:name:object: 外,还有个 addObserverForName:object:queue:usingBlock:,这两种方式有什么区别呢?

NSNotification 的发送和通知接收回调是在同一线程的,这点可以通过一个例子验证下:

1
2
3
4
5
6
7
8
9
10
- (IBAction)btnClick:(id)sender {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"发送通知:%@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationName object:nil];
});
}

- (void)notificationAction:(NSNotification *)notification {
NSLog(@"收到通知:%@", [NSThread currentThread]);
}

打印结果:

1
2
收到通知:<NSThread: 0x6000036a2900>{number = 7, name = (null)}
发送通知:<NSThread: 0x6000036a2900>{number = 7, name = (null)}

但是,我们有时候是在子线程发出通知,想要在收到通知的时候在主线程更新 UI,除了在收到通知后主动切换到主线程外,还可以使用 addObserverForName:object:queue:usingBlock: 方法实现,该方法可以指定一个收到通知的 block 在哪个队列执行,我们可以指定主队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad {
[super viewDidLoad];

[[NSNotificationCenter defaultCenter] addObserverForName:kNotificationName object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"收到通知:%@", [NSThread currentThread]);
}];
}

- (IBAction)btnClick:(id)sender {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"发送通知:%@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationName object:nil];
});
}

打印结果:

1
2
发送通知:<NSThread: 0x600001420f00>{number = 5, name = (null)}
收到通知:<NSThread: 0x600001464880>{number = 1, name = main}

可以看到,我们在子线程发出通知,但是收到通知的 block 是在主线程的。

这两个方法添加监听者还有一个很重要的区别,就是在 iOS 9 之后,addObserver:selector:name:object: 添加的监听者,在监听者对象释放之前不再需要手动移除监听(iOS 9 之前是需要的)。这是因为在 iOS 9 以后,通知中心持有的观察者由 unsafe_unretained 引用变为 weak 引用,unsafe_unretained 就是 iOS 5 版本以下的 assignassignunsafe_unretained 修饰对象会产生野指针的问题,修饰的对象释放后,指针不会自动置成 nil,此时再向对象发消息程序会崩溃。而 weak 会在对象释放后置为 nil,所以 iOS 9 之后,不移除监听者也不会引起访问野指针的 Crash 了。

Apple 文档涉及到这块的描述:

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. Otherwise, you should call removeObserver:name:object: before observer or any object passed to this method is deallocated.

addObserverForName:object:queue:usingBlock: 方法添加的监听在 iOS 9 之后仍然需要手动移除监听,因为 NSNotificationCenter 对该方法注册的监听者是强引用,如不移除虽然不会引起 Crash,但是会导致内存泄露。

Apple 文档涉及到这块的描述:

To unregister observations, you pass the object returned by this method to removeObserver:. You must invoke removeObserver: or removeObserver:name:object:before any object specified by addObserverForName:object:queue:usingBlock:is deallocated.

文档中也已经指出,addObserverForName:object:queue:usingBlock: 会有一个返回值,将该返回值传给 removeObserver: 方法即可移除监听。

3、NSNotificationQueue

NSNotificationQueue 用于通知消息的管理,如消息发送时机、消息合并策略,并且为 FIFO(先入先出)方式管理消息,但实际消息的发送仍然是在指定时机自动使用 NSNotificationCenter 发送的。每个线程都有一个默认的通知队列,即 NSNotificationQueue,它与 APP 默认的 NSNofitifcationCenter 关联。

NSNotificationQueue 中定义的属性与方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取线程默认的 NSNotificationQueue
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;

// 创建新的 NSNotificationQueue 并指定 NSNotificationCenter
- (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;

向队列中添加通知时的参数含义如下:
postingStyle:用于配置通知什么时候发送

  • NSPostASAP:Posting As Soon As Possible,在当前通知调用或者计时器结束发送通知。发送时机早于 NSPostWhenIdlekCFRunLoopBeforeTimerskCFRunLoopBeforeSources 之间发送通知。
  • NSPostWhenIdle:当 runloop 处于空闲时发出通知。在 kCFRunLoopBeforeWaitingkCFRunLoopAfterWaiting 之间发生通知。
  • NSPostNow:在合并通知完成之后立即发出通知。

需要注意的是,如果 postingStyle 参数不是 NSPostNow,并且是在子线程,需要保证子线程 runloop 已经开启;如果是 NSPostNow,则和直接使用 NSNotificationCenter 发送通知效果一样无需开启 runloop。

coalesceMask(注意这是一个 NS_OPTIONS 参数):用于配置如何合并通知,即移除重复的通知

  • NSNotificationNoCoalescing:不合并通知(无论 name 和 object 是否相同都不移除通知)
  • NSNotificationCoalescingOnName:按照通知名字合并通知
  • NSNotificationCoalescingOnSender:按照传入的 object 合并通知

modes:runloop 的 mode,指定 mode 后,只有当前线程的 runloop 在指定的 mode 下才能将通知自动发送到通知中心。

例如,将通知添加到队列,并指定 runloop 空闲时发送通知:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
static NSString * const kNotificationName = @"kNotificationName";

@interface ViewController ()

@property (nonatomic, strong) NSThread *thread;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 添加监听
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(notificationAction:) name:kNotificationName object:nil];
}

- (void)notificationAction:(NSNotification *)notification {
NSLog(@"收到通知:%@", [NSThread currentThread]);
}

// 按钮点击事件
- (IBAction)btnClick:(id)sender {
[self performSelector:@selector(postNotification) onThread:self.thread withObject:nil waitUntilDone:YES];
}

- (void)postNotification {
NSNotification *notification = [NSNotification notificationWithName:kNotificationName object:nil];
// 将通知添加到队列,并指定 runloop 空闲时发送通知,不合并通知
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
}

- (NSThread *)thread {
if (!_thread) {
_thread = [[NSThread alloc] initWithBlock:^{

// 添加 runloop 状态监听
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
CFRelease(observer);

// 线程保活
[[NSRunLoop currentRunLoop] run];
}];
[_thread start];
}
return _thread;
}

@end

按钮点击打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
kCFRunLoopEntry
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopExit
kCFRunLoopEntry
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
收到通知:<NSThread: 0x600000f13c80>{number = 7, name = (null)}
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting

可以看到,如果我们指定了在 runloop 空闲时发送通知,则会在 runloop 即将进入休眠时发送通知。

二、NSNotification 实现原理

由于苹果对 NSNotification 是不开源的,GNUstep 将 Cocoa 的 OC 库重新开源实现了一遍,虽然 GNUstep(https://github.com/gnustep/libs-base) 不是苹果官方源码,但还是具有一定的参考价值。

根据 GNUstep 相关源码可知,NSNotificationCenter 中持有了一个 _table

1
2
3
4
5
6
7
8
9
@interface NSNotificationCenter : NSObject
{
@private
void *_table;
}

// ......

@end

在 NSNotificationCenter.m 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@implementation NSNotificationCenter

// ...

- (id) init {
if ((self = [super init]) != nil) {
_table = newNCTable();
}
return self;
}

static NCTable *newNCTable(void) {
NCTable *t;
// ...
return t;
}

// ...

@end

可以知道,NSNotificationCenter 持有了一个 NCTable 类型的 _tableNCTable 是一个结构体,简化后的结构如下:

1
2
3
4
5
6
typedef struct NCTbl {
Observation *wildcard; // 链表结构,保存既没有 name 也没有 object 的通知
GSIMapTable nameless; // 存储无 name 、有 object 的通知
GSIMapTable named; // 存储有 name 的通知(不管有没有 object)
// ...
} NCTable;

其中 Observation 结构如下:

1
2
3
4
5
6
7
typedef struct Obs {
id observer; // 保存接受消息的对象
SEL selector; // 保存注册通知时传入的 SEL
struct Obs *next; // 保存注册了同一个通知的下一个观察者
struct NCTbl *link; // 保存该 Observation 的 NCTable
// ...
} Observation;

关于其中 GSIMapTable 等详细结构,接下来从源码实现上来看。

1、addObserver:selector:name:object: 方法的实现

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
- (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);

// case 1: 有 name,存储到 named 表
if (name) {
// NAMED 是个宏:(NCTable*)_table->named
// 以 name 为 key,从 named 表中获取对应的 GSIMapNode 类型的 value
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
if (n == 0) {
// 不存在,则创建
m = mapNew(TABLE);
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
...
}
else {
// 存在则把值取出来 赋值给 m
m = (GSIMapTable)n->value.ptr;
}
// 以 object 为 key,从字典 m 中取出对应的 GSIMapNode 类型的 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;
}
}
// case2:无 name 、有 object,存储到 nameless 表
else if (object) {
// NAMELESS 也是个宏:(NCTable*)_table->nameless
// 以 object 为 key,从 nameless 表中取出对应的 value(Observation 类型)
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
// 不存在则新建链表,并存到 map 中
if (n == 0) {
o->next = ENDOBS;
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}
else { // 存在 则把值接到链表的节点上
...
}
}
// case3:无 name & 无 object,则存储到 wildcard 链表
else {
// WILDCARD 也是个宏:(NCTable*)_table->wildcard
o->next = WILDCARD;
WILDCARD = o;
}
}

该流程总结:

根据以上各字段数据结构可知,无论是否存在 nameobject,最终监听者等信息都是存储在 Observation 链表当中的,只不过获取该 Observation 链表的路径不一样。

  • 创建 Observation,添加监听者实际上就是存储 Observation 的过程。

    • Observation 是一个链表,其中存储了 observerselector 信息,以及指向下一个 Observationnext 指针。
  • NSNotificationCenter 持有了一个 NCTable 类型的 _table,根据添加监听时 nameobject 参数的情况,决定 Observation 存储在 NCTable 中的哪一个字段中。

  • 如果 name 存在:

    • 则向 named 表中插入元素,keynamevalueGSIMaptable
    • GSIMaptablekeyobjectvalueObservation
  • 如果 name 不存在:

    • 则向 nameless 表中插入元素,keyobjectvalueObservation
  • 如果 nameobject 都不存在:

    • 则把这个 Observation 添加 WILDCARD 链表中。

这里需要注意的是,如果监听者重复注册了多次监听,通知发送时将会收到多次通知。

2、addObserverForName:object:queue:usingBlock: 方法的实现

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
31
32
33
34
35
36
- (id) addObserverForName: (NSString *)name 
object: (id)object
queue: (NSOperationQueue *)queue
usingBlock: (GSNotificationBlock)block
{
// 创建一个临时监听者
GSNotificationObserver *observer =
[[GSNotificationObserver alloc] initWithQueue: queue block: block];

// 调用了 addObserver:selector:name:object: 注册监听者
[self addObserver: observer
selector: @selector(didReceiveNotification:)
name: name
object: object];

// 返回监听者
return observer;
}


- (void) didReceiveNotification: (NSNotification *)notif
{
if (_queue != nil)
{
// 如果 queue 不为空,在该队列中执行 block;
GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc]
initWithNotification: notif block: _block];

[_queue addOperation: op];
}
else
{
// 如果 queue 为空,直接在当前线程执行 block;
CALL_BLOCK(_block, notif);
}
}

根据该方法源码可以看到该方法的实现就比较简单了:

  • 创建一个临时监听者,并调用 addObserver:selector:name:object: 方法注册监听者
  • 监听者收到通知时,在指定的队列 queue 中执行 block。如果 queue 为空,直接在当前线程执行 block

3、postNotificationName:object:userInfo: 方法的实现

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
31
32
33
34
- (void) postNotificationName: (NSString*)name
object: (id)object
userInfo: (NSDictionary*)info
{
// 构造 GSNotification 对象(NSNotification 子类),并赋值 name、object、info
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;
if (o->next != 0){
[o->observer performSelector: o->selector
withObject: notification];
}

}

// step3: 释放资源
RELEASE(notification);
}

以上逻辑也比较简单:

  • 创建 notification 对象
  • 根据 name & object 查找所有符合的 Observation,并保存到数组中。
  • 遍历数组,使用 performSelector: 逐一同步调用 sel
  • 释放 notification 对象

三、NSNotification 与多线程

1、NSNotification 的发送和通知接收回调是在同一线程的

这点前面已经使用一个例子验证了,我们有时候是在子线程发出通知,想要在收到通知的时候在主线程更新 UI,除了在收到通知后主动切换到主线程外,还可以使用 addObserverForName:object:queue:usingBlock: 方法指定在主队列执行 block 的方式去实现。

其实 Apple 官方文档里还介绍了一种实现方案:使用 Mach Port 将通知转发到指定线程。假设在子线程发送通知,想在主线程处理通知,使用该方案的实现如下:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#import "ViewController.h"

static NSString * const kNotificationName = @"kNotificationName";

@interface ViewController () <NSMachPortDelegate>

// 通知队列
@property (nonatomic) NSMutableArray *notifications;
// 期望线程
@property (nonatomic) NSThread *notificationThread;
// 用于对通知队列加锁的锁对象,避免线程冲突
@property (nonatomic) NSLock *notificationLock;
// 向期望线程发送信号的通信端口(Mach Port)
@property (nonatomic) NSMachPort *notificationPort;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 初始化
self.notifications = [[NSMutableArray alloc] init];
self.notificationLock = [[NSLock alloc] init];

// 期望线程为主线程
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];

// 添加端口代理,收到 Mach Message 后会执行 handleMachMessage: 方法
self.notificationPort.delegate = self;

// 往当前线程(主线程)的 runloop 添加端口源
[[NSRunLoop currentRunLoop] addPort:self.notificationPort
forMode:(__bridge NSString *)kCFRunLoopCommonModes];

// 添加监听者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:kNotificationName object:nil];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在子线程发送通知
NSLog(@"子发送通知:%@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationName object:nil userInfo:nil];
});
}

// 从端口收到 Mach Message
- (void)handleMachMessage:(void *)msg {

[self.notificationLock lock];

// 遍历 notifications 数组中全部通知并发送
while ([self.notifications count]) {
NSNotification *notification = [self.notifications objectAtIndex:0];
[self.notifications removeObjectAtIndex:0];
[self.notificationLock unlock];
// 将 notification 转发到期望线程(主线程)
// 因为收到 Mach Message 的回调一定是在期望线程的 runloop 上(即期望线程上),所以这里直接调用原 notification 处理方法即可
[self processNotification:notification];
[self.notificationLock lock];
};

[self.notificationLock unlock];
}

// 收到通知
- (void)processNotification:(NSNotification *)notification {

if ([NSThread currentThread] != _notificationThread) {
// 如果收到通知的线程不是期望线程
[self.notificationLock lock];
// 将通知添加到 notifications 数组
[self.notifications addObject:notification];
[self.notificationLock unlock];

NSLog(@"需要转发,因为在非期望线程收到通知:%@", [NSThread currentThread]);

// 从指定 Mach Port 发送一条 Message,只需设置发送时间参数即可
[self.notificationPort sendBeforeDate:[NSDate date]
components:nil
from:nil
reserved:0];
} else {
// 收到通知的线程是期望线程,继续后续处理逻辑
NSLog(@"在期望线程收到通知:%@", [NSThread currentThread]);
}
}

@end

打印结果:

1
2
3
子发送通知:<NSThread: 0x600002fb8a40>{number = 8, name = (null)}
需要转发,因为在非期望线程收到通知:<NSThread: 0x600002fb8a40>{number = 8, name = (null)}
在期望线程收到通知:<NSThread: 0x600002ff4900>{number = 1, name = main}

实现逻辑总结如下:

  • 首先定义一个 notifications 数组,用于保存需要转发的 notification

  • 在期望线程的 runloop 上添加一个 Mach Port 端口源

  • 收到通知时判断是否是在期望线程收到的

    • 如果是则直接处理;
    • 如果不是,则将该 notification 保存到 notifications 数组中,并通过 Mach Port 端口发送一条 Message
  • 由于 Mach Port 端口源是添加到期望线程 runloop 上的,所以收到 Mach Message 的 delegate 方法 handleMachMessage: 一定是在期望线程上被执行的

  • handleMachMessage: 中遍历 notifications 数组中所有 notification,以 notification 为参数重新执行收到通知的处理方法即可

2、NSNotification 的发送和处理是同步的

NSNotification 的发送和处理是同步的,即 NSNotification 的所有监听者注册 sel 执行完毕后,才会执行发送 NSNotification 后面的后续逻辑,例如:
定义一个 TestObject,用于用于监听与处理通知,并且在收到通知的 sel 中休眠 2 秒:

1
2
3
4
5
6
7
8
9
10
11
#import "TestObject.h"

@implementation TestObject

- (void)notificationAction:(NSNotification *)notification {
NSLog(@"收到通知");
sleep(2);
NSLog(@"休眠 2 秒");
}

@end

然后创建三个 TestObject 的实例监听与处理通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)viewDidLoad {
[super viewDidLoad];

_obj1 = [[TestObject alloc] init];
_obj2 = [[TestObject alloc] init];
_obj3 = [[TestObject alloc] init];

[[NSNotificationCenter defaultCenter] addObserver:_obj1 selector:@selector(notificationAction:) name:kNotificationName object:nil];
[[NSNotificationCenter defaultCenter] addObserver:_obj2 selector:@selector(notificationAction:) name:kNotificationName object:nil];
[[NSNotificationCenter defaultCenter] addObserver:_obj3 selector:@selector(notificationAction:) name:kNotificationName object:nil];
}

- (IBAction)btnClick:(id)sender {
NSLog(@"点击了按钮,发送通知");
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationName object:nil];
NSLog(@"按钮点击的最后一个打印");
}

打印结果:

1
2
3
4
5
6
7
8
点击了按钮,发送通知
收到通知
休眠 2 秒
收到通知
休眠 2 秒
收到通知
休眠 2 秒
按钮点击的最后一个打印

根据打印结果可以看到,发送 Notification 会阻塞线程,直到所有监听者注册的 sel 执行完毕才会继续往下执行,即 Notification 的发送和处理是同步的。

3、异步发送通知

根据上面的例子可以知道,NSNotification 的发送和处理是同步的,那是否有办法异步发送通知呢?即发送 Notification 如何不阻塞线程?

可以借助前面提到的 NSNotificationQueue 来实现异步发送通知,实际上从线程的角度看并不是真正的异步发送,因为不涉及开启新线程,更应当称之为延时发送。

例如,在主线程发送通知,仍然在主线程收到通知,但是使发送通知不阻塞主线程。可以使用 NSNotificationQueue 将通知延迟到 runloop 空闲时发送,在上面例子基础上修改下通知发送方式即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)viewDidLoad {
[super viewDidLoad];

_obj1 = [[TestObject alloc] init];
_obj2 = [[TestObject alloc] init];
_obj3 = [[TestObject alloc] init];

[[NSNotificationCenter defaultCenter] addObserver:_obj1 selector:@selector(notificationAction:) name:kNotificationName object:nil];
[[NSNotificationCenter defaultCenter] addObserver:_obj2 selector:@selector(notificationAction:) name:kNotificationName object:nil];
[[NSNotificationCenter defaultCenter] addObserver:_obj3 selector:@selector(notificationAction:) name:kNotificationName object:nil];
}

- (IBAction)btnClick:(id)sender {
NSLog(@"点击了按钮,发送通知");
NSNotification *notification = [NSNotification notificationWithName:kNotificationName object:nil];
// 将通知添加到队列(这里获取到的就是主队列),并指定 runloop 空闲时发送通知,不合并通知
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
NSLog(@"按钮点击的最后一个打印");
}

打印结果:

1
2
3
4
5
6
7
8
点击了按钮,发送通知
按钮点击的最后一个打印
收到通知
休眠 2 秒
收到通知
休眠 2 秒
收到通知
休眠 2 秒

根据打印结果可以看到,该方式发送通知并没有阻塞线程,不需要等待所有监听者处理完通知即可执行后续逻辑。