NSNotification

5/21/2021

通知是同步的, 发送通知的流程:总的来说,就是根据Notification Name查找对应的Observer链表,然后遍历整个链表,给每个Observer节点中保持的对象及SEL发送消息,也就是调用对象的SEL方法
首先会定一个数组ObserverArray来保存需要通知的Observer。先遍历wildcard链表,将其中所有的Observer加入到数组中(因为wildcard链表中的observe接收所有的通知)
找到以object为key的Observer链表。这个过程分为在Named Table和UnNamed Table中查找,然后将遍历查找到的链表,同样加入到数组中。
遍历数组,依次取出节点发送Notification

# 主要类

# Notification

对通知的本身信息进行封装,比如通知的name,发送对象,额外信息等。
一般直接使用NotificationCenter调用[[NSNotificationCenter defaultCenter] postNotificationName:@"xxx" object:nil userInfo:nil], 方法内部直接创建Notification对象并发出通知。

# NotificationCenter

对整个通知的流程进行管理,比如添加观察者,发送通知,移除观察者。
添加观察者: addObserver:selector:name:object:
addObserverForName:object:queue:usingBlock:
移除观察者:
removeObserver:
removeObserver:name:object:
发出通知:
postNotification:
postNotificationName:object:
postNotificationName:object:userInfo:

# NSNotificationQueue

提供更加灵活的设置,比如设置通知的合并策略,发送时机。
在NSNotificationCenter中起到一个缓冲作用。放入队列中的通知可能会延迟,直到当前的runloop结束或者runloop处于空闲状态才发送,具体的要根据设置的策略而定。
NSPostingStyle: 用于配置发送时机

  • NSPostASAP:在当前通知调用或者计时器结束时发出通知
  • NSPostWhileIdle:当runloop处于空闲时发出通知
  • NSPostNow:在合并通知完成后立即发出通知

NSNotificationCoalescing: 这是一个NS_OPTION,用于配置如何合并通知

  • NSNotificationNoCoalescing:不合并通知
  • NSNotificationCoalescingOnName:按通知名字合并
  • NSNotificationCoalescingOnSender:按传人的object合并通知

# 实现原理

# 数据结构关系

NSNotificationCenter定义了两个Table(nameless和named),同时为了封装观察者信息,也定义了Observation保存观察者信息。他们的结构体可以简化如下所示:

// 根容器,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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

其中GSIMap的结构如下:

typedef struct _GSIMapBucket GSIMapBucket_t;
typedef struct _GSIMapNode GSIMapNode_t;

typedef GSIMapBucket_t *GSIMapBucket;
typedef GSIMapNode_t *GSIMapNode;

typedef struct _GSIMapTable GSIMapTable_t;
typedef GSIMapTable_t *GSIMapTable;

struct	_GSIMapNode {
    GSIMapNode	nextInBucket;	/* Linked list of bucket.	*/
    GSIMapKey	key;
#if	GSI_MAP_HAS_VALUE
    GSIMapVal	value;
#endif
};

struct	_GSIMapBucket {
    uintptr_t	nodeCount;	/* Number of nodes in bucket.	*/
    GSIMapNode	firstNode;	/* The linked list of nodes.	*/
};

struct	_GSIMapTable {
  NSZone	*zone;
  uintptr_t	nodeCount;	/* Number of used nodes in map.	*/
  uintptr_t	bucketCount;	/* Number of buckets in map.	*/
  GSIMapBucket	buckets;	/* Array of buckets.		*/
  GSIMapNode	freeNodes;	/* List of unused nodes.	*/
  uintptr_t	chunkCount;	/* Number of chunks in array.	*/
  GSIMapNode	*nodeChunks;	/* Chunks of allocated memory.	*/
  uintptr_t	increment;
#ifdef	GSI_MAP_EXTRA
  GSI_MAP_EXTRA	extra;
#endif
};
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

GSIMapTable映射表包含了指向GSIMapNode单链表节点的指针数组nodeChunks,通过buckets数组记录单链表节点指针数组的各个链表的节点数量及链表首部地址,其中bucketCount、nodeCount及chunkCount分别记录了node节点、节点单链表信息数组、节点单链表指针数组的数目;
其实就是一个hash表结构,既可以以数组的形式取到每个单链表首元素,也可以以链表的形式获取,通过数组能够方便取到每个单向链表,再利用链表结构增删。

在NSNotificationCenter内部一共保存了两张表,一张用于保存添加观察者的时候传入的NotificationName的情况;一张用于保存添加观察者的时候没有传入NotificationName的情况

# Named Table

结构为两层Table嵌套,外层以通知名称为Key,其value同样是一个Table,内层Table以Object为key,用链表保存所有的观察者,并且以这个链表为value

  • key(name)
  • value(mapTable)
    • key(object)
    • value(Observation对象)

Named Table

# UnNamed Table

相对于Named Table,少了一层外层Table

UnNamed Table

如果在注册观察者时没有传入NotificationName,同时没有传入object,所有的系统通知都会发送到注册的对象里, 即wildcard链表中所有的对象。

# 添加观察者流程

添加观察者一般调用方法[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onEventNotification:) name:NotificationName object:nil];来实现,大致流程如下:

Notifcation add

# 发送通知流程

发送通知一般是调用方法postNotificationName:object:userInfo:,该方法会实例化一个NSNotification对象来保存传入的参数,包括:name,Object和userInfo。总的来说,就是根据Notification Name查找对应的Observer链表,然后遍历整个链表,给每个Observer节点中保持的对象及SEL发送消息,也就是调用对象的SEL方法,大致流程如下:

Notifcation add

从发送过程来看,发送通知和接收通知是在同一个线程中同步执行的。

# 通知与多线程

从通知发送流程来看,我们知道通知的发送和接收是同步执行的,即当在主线程中发出通知时,然后接收方在主线程处理逻辑,处理完成后,发送方才能继续执行剩下的逻辑。
那Notification和线程同步有什么关系呢?

In a multithreaded application, notifications are always delivered in the thread in which the notification was posted, which may not be the same thread in which an observer registered itself.

在多线程应用程序中,通知总是在发出通知的线程中传递,而该线程不一定是观察者观察者的那个线程。

For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.

例如,如果在后台线程中运行的对象正在监听来自用户界面的通知,例如窗口关闭,则希望在后台线程而不是主线程中接收通知。在这些情况下,您必须在默认线程上传递通知时捕获它们,并将它们重定向到适当的线程中。

常见的面试问题是:如何保证通知接收的线程在主线程?

  1. 使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block
  2. 在主线程注册一个machPort,它是用来做线程通信的,设置这个port的delegate,通过这个Port其他线程可以跟主线程通信,在这个port的代理回调中执行的代码肯定在主线程中运行,所以当在异步线程收到通知,然后给machPort发送消息即可
//
//  YKNoteNotificationViewController.m
//  YKNote
//
//  Created by wanyakun on 2021/5/26.
//  Copyright © 2021 wanyakun.github.io. All rights reserved.
//

#import "YKNoteNotificationViewController.h"

@interface YKNoteNotificationViewController ()<NSMachPortDelegate>

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

@end

@implementation YKNoteNotificationViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.title = @"Notification";
    self.view.backgroundColor = [UIColor whiteColor];
    
    NSLog(@"...");
    NSLog(@"current thread = %@", [NSThread currentThread]);
    
    self.notifications = [[NSMutableArray alloc] initWithCapacity:5];
    self.notificationLock = [[NSLock alloc] init];
    self.notificationThread = [NSThread currentThread];
    self.notificationPort = [[NSMachPort alloc] init];
    self.notificationPort.delegate = self;
    
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort forMode:NSRunLoopCommonModes];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"KNotificationTest" object:nil];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"KNotificationTest" object:nil userInfo:nil];
    });
}

- (void)processNotification:(NSNotification *)notifcation {
    if ([NSThread currentThread] != self.notificationThread) {
        // 转发到正确的线程
        [self.notificationLock lock];
        [self.notifications addObject:notifcation];
        [self.notificationLock unlock];
        
        [self.notificationPort sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];
    } else {
        // 在这里处理通知
        NSLog(@"current thread = %@", [NSThread currentThread]);
        NSLog(@"process notifcation");
    }
}

#pragma mark - NSMachPortDelegate
- (void)handleMachMessage:(void *)msg {
    [self.notificationLock lock];
    while (self.notifications.count > 0) {
        NSNotification *notification = [self.notifications objectAtIndex:0];
        [self.notifications removeObjectAtIndex:0];
        [self.notificationLock unlock];
        [self processNotification:notification];
        [self.notificationLock lock];
        
    }
    [self.notificationLock unlock];
}

@end
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

# 常见问题

  1. 通知的发送是同步的,还是异步的?
    同步发送
  2. NSNotifactionCenter接收消息和发送消息是在同一个线程里吗?如何异步发送消息?
    是的,是在同一个线程中,异步线程发送通知则响应函数也是在异步线程。
    异步发送通知可以开启异步线程发送即可。
  3. NSNotifacionQueue是异步还是同步发送?在哪个线程响应?
    通知队列,用于异步发送消息,这个异步并不是开启线程,而是把通知存到双向链表实现的队列里面,等待某个时机触发时调用NSNotificationCenter的发送接口进行发送通知,这么看NSNotificationQueue 最终还是调用NSNotificationCenter进行消息的分发
    依赖runloop,所以如果在其他子线程使用NSNotificationQueue,需要开启runloop,最终还是通过NSNotificationCenter进行发送通知,所以这个角度讲它还是同步的,所谓异步,指的是非实时发送而是在合适的时机发送(延时发送),并没有开启异步线程
  4. NSNotifacionQueue和Runloop的关系?
    NSNotificationQueue依赖runloop. 因为通知队列要在runloop回调的某个时机调用通知中心发送通知,NSNotificationQueue主要做了两件事:
  • 添加通知到队列
  • 删除通知
  1. 如何保证通知接收的线程在主线程?
    参考通知与多线程
  2. 页面销毁时不移除通知会崩溃吗?
    iOS9.0之前,会crash,原因:通知中心对观察者的引用是unsafe_unretained,导致当观察者释放的时候,观察者的指针值并不为nil,出现野指针.
    iOS9.0之后,不会crash,原因:通知中心对观察者的引用是weak。
    当 iOS 9.0+ 时,不移除通知不会崩溃;当低于 iOS 9.0 时,需要手动移除通知。
    使用 addObserverForName:object:queue:usingBlock: 添加的通知,即使不移除,也不会崩溃,但是造成内存泄漏。
  3. 多次添加同一个通知会是什么结果?多次移除通知呢?
    多次添加同一个通知,会导致发送一次这个通知,响应多次通知回调
    多次移除通知不会产生crash
  4. 下面的方式能接收到通知吗?为什么?
// 发送通知  
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知  
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
1
2
3
4

存储是以name和object为维度的,即判定是不是同一个通知要从name和object区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都是以这两个为维度的
故而不会。

Last Updated: 10/25/2024, 6:55:06 AM