通知传值概要
-
通知传值可以跨越多个界面进行传值,一般用于后一个界面向前一个界面传值。
-
通知传值支持多个接收者,多个对象可以同时接收同一个通知并进行处理。这样可以实现一对多的通信,方便跨多个对象进行值传递。
使用步骤
1.在发送者中创建并发送通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"identification" object:nil userInfo:@{@"content": self.myTextField.text}];
2.在接收者中注册观察者并接受通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(Notificate:) name:@"identification" object:nil];
3.在使用完或不使用时移除观察者
[[NSNotificationCenter defaultCenter] removeObserver:self];
通知机制关键类
NSNotification
用于描述通知的类,一个NSNotification
对象就包含了一条通知的信息,所以当创建一个通知时通常包含如下属性:
@interface NSNotification : NSObject <NSCopying, NSCoding>
...
/* Querying a Notification Object */
- (NSString*) name; // 通知的name
- (id) object; // 携带的对象
- (NSDictionary*) userInfo; // 配置信息
@end
NSNotificationCenter
通知机制的核心是一个与线程关联的单例对象叫通知中心(NSNotificationCenter
)。通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue
)异步发送通知。
static NSNotificationCenter *default_center = nil;
+ (NSNotificationCenter*) defaultCenter
{
return default_center;
}
NSNotificationCenter
类主要负责三件事:
- 添加通知
- 发送通知
- 移除通知
// 添加通知
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 发送通知
- (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;
通知中心定义了两个结构体来存储通知信息和观察者信息:
// 根容器,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;
named表
在 named 表中,NotifcationName 作为表的 key,table作为表的value。因为我们在注册观察者的时候是可以传入一个参数 object 用于只监听指定该对象发出的通知,并且一个通知可以添加多个观察者,所以还需要一张表来保存 object 和 Observer 的对应关系。这张表的 key、Value 分别是以 object 为 Key,Observer 为 value。用了链表这种数据结构实现保存多个观察者的情况。
在实际开发过程中 object 参数我们经常传 nil,这时候系统会根据 nil 自动生成一个 key,相当于这个 key 对应的 value(链表)保存的就是当前通知传入了 NotificationName 没有传入 object 的所有观察者。
nameless表
nameless 表没有 NotificationName ,即没有了最外边一层键值对的约束了,其中就只有 object 和 Observation 所对应的键值对结构了:
wildcard表
这个表既没有 NotificationName 也没有 object 了,所以他就会在 nameless基础上在脱去一层键值对,那么它就只剩下一个链表了,该练表存储了可以接收所有通知的类的信息
NSNotificationQueue
通知队列,用于异步发送消息,这个异步并不是开启线程,而是把通知存到双向链表实现的队列里面,等待某个时机触发时调用NSNotificationCenter
的发送接口进行发送通知,这么看NSNotificationQueue
最终还是调用NSNotificationCenter
进行消息的分发
NSNotificationQueue
是依赖runloop
的,所以如果线程的runloop
未开启则无效
NSNotificationQueue
主要做了两件事:
- 添加通知到队列
- 删除通知
// 把通知添加到队列中,NSPostingStyle是个枚举,下面会介绍
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 删除通知,把满足合并条件的通知从队列中删除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
把通知添加到队列等待发送,同时提供了一些附加条件供开发者选择,如:什么时候发送通知、如何合并通知等,系统给了如下定义:
// 表示通知的发送时机
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1, // runloop空闲时发送通知
NSPostASAP = 2, // 尽快发送,这种情况稍微复杂,这种时机是穿插在每次事件完成期间来做的
NSPostNow = 3 // 立刻发送或者合并通知完成之后发送
};
// 通知合并的策略,有些时候同名通知只想存在一个,这时候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0, // 默认不合并
NSNotificationCoalescingOnName = 1, // 只要name相同,就认为是相同通知
NSNotificationCoalescingOnSender = 2 // object相同
};
同步通知机制实现
注册通知
使用方法addObserver:selector:name:object
添加观察者
- (void) addObserver: (id)observer
selector: (SEL)selector
name: (NSString*)name
object: (id)object
{
Observation *list;
Observation *o;
GSIMapTable m;
GSIMapNode n;
// observer为空时的报错
if (observer == nil)
[NSException raise: NSInvalidArgumentException
format: @"Nil observer passed to addObserver ..."];
// selector为空时的报错
if (selector == 0)
[NSException raise: NSInvalidArgumentException
format: @"Null selector passed to addObserver ..."];
// observer不能响应selector时的报错
if ([observer respondsToSelector: selector] == NO)
{
[NSException raise: NSInvalidArgumentException
format: @"[%@-%@] Observer '%@' does not respond to selector '%@'",
NSStringFromClass([self class]), NSStringFromSelector(_cmd),
observer, NSStringFromSelector(selector)];
}
// 给表上锁
lockNCTable(TABLE);
// 创建一个observation对象,持有观察者和SEL,下面进行的所有逻辑就是为了存储它
o = obsNew(TABLE, selector, observer);
/*======= case1: 如果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,其实value被MapNode的结构包装了一层,这里不追究细节
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不为空 =======*/
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 { // 存在 则把值接到链表的节点上
...
}
}
/*======= case3:name 和 object 都为空 则存储到wildcard链表中 =======*/
else {
o->next = WILDCARD;
WILDCARD = o;
}
// 解锁
unlockNCTable(TABLE);
}
NCTable
结构体中核心的三个变量以及功能:wildcard
、named
、nameless
,在源码中直接用宏定义表示了:WILDCARD
、NAMELESS
、NAMED
存在name
(无论object是否存在)
-
找到
NCTable
中的named
表,这个表存储了还有name
的通知 -
以
name
作为key,找到value
,这个value
依然是一个map
-
在
map
的结构以object
作为key,Observation
对象为value,这个Observation
对象的结构是链表结构,主要存储了observer & SEL
-
然后把刚开始创建的
Observation
对象o
存储进去
name不存在,只存在object
- 以
object
为key,从nameless
表中取出value,此value是个Observation
类型的链表 - 把创建的
Observation
类型的对象o
存储到链表中
没有name和object
直接把Observation
对象存放在了Observation *wildcard
链表结构中
发送通知
使用方法postNotification
:, postNotificationName:object:userInfo
或者postNotificationName:object:
发送通知,后者默认userInfo
为nil
// 发送通知
- (void) postNotification: (NSNotification*)notification {
if (notification == nil) {
[NSException raise: NSInvalidArgumentException
format: @"Tried to post a nil notification."];
}
[self _postAndRelease: RETAIN(notification)];
}
- (void) postNotificationName: (NSString*)name
object: (id)object {
[self postNotificationName: name object: object userInfo: nil];
}
- (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];
}
最终都只会调用 _postAndRelease:
方法。不同的是,postNotification:
方法外部直接传了一个NSNotification
对象,其他两个方法都是内部进行了处理包装 成为了一个NSNotification
对象,我们再看看_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;
// name为空的报错,注册时可以注册无名,注册无名就等于说是所有的通知都能接收,但是发送通知时不可以
if (name == nil) {
RELEASE(notification);
[NSException raise: NSInvalidArgumentException
format: @"Tried to post a notification with no name."];
}
object = [notification object];
GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);
lockNCTable(TABLE);
// 查找所有未指定name或object的观察者,加在a数组中,即将wildcard表中的数据都加在新建链表中
for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next)
{
GSIArrayAddItem(a, (GSIArrayItem)o);
}
// 查找与通知的object相同但是没有name的观察者,加在a数组中
if (object) {
// 在nameless中找object对应的数据节点
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
if (n != 0) { // 将其加入到新建链表中
o = purgeCollectedFromMapNode(NAMELESS, n);
while (o != ENDOBS) {
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
}
// 查找name的观察者,但观察者的非零对象与通知的object不匹配时除外,加在a数组中
if (name) {
// 先匹配name
n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
if (n) { // m指向name匹配到的数据
m = (GSIMapTable)n->value.ptr;
} else {
m = 0;
}
if (m != 0) { // 如果上述name查找到了数据
// 首先,查找与通知的object相同的观察者
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n != 0) { // 找到了与通知的object相同的观察者,就加入到新建链表中
o = purgeCollectedFromMapNode(m, n);
while (o != ENDOBS) {
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
if (object != nil) {
// 接着是没有object的观察者,都加在新建链表中
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);
if (n != 0) { // 如果没有object并且有数据,就把其中的数据加到新建链表中
o = purgeCollectedFromMapNode(m, n);
while (o != ENDOBS) {
GSIArrayAddItem(a, (GSIArrayItem)o);
o = o->next;
}
}
}
}
}
unlockNCTable(TABLE);
// 发送通知,给之前新建链表中的所有数据
count = GSIArrayCount(a);
while (count-- > 0) {
o = GSIArrayItemAtIndex(a, count).ext;
if (o->next != 0) {
NS_DURING {
// 给observer发送selector,让其处理
[o->observer performSelector: o->selector
withObject: notification];
}
NS_HANDLER {
BOOL logged;
// 尝试将通知与异常一起报告,但是如果通知本身有问题,我们只记录异常。
NS_DURING
NSLog(@"Problem posting %@: %@", notification, localException);
logged = YES;
NS_HANDLER
logged = NO;
NS_ENDHANDLER
if (NO == logged)
{
NSLog(@"Problem posting notification: %@", localException);
}
}
NS_ENDHANDLER
}
}
lockNCTable(TABLE);
GSIArrayEmpty(a);
unlockNCTable(TABLE);
RELEASE(notification);
}
上面的代码精简后如下:
//发送通知的核心函数,主要做了三件事:查找通知、发送、释放资源
- (void) _postAndRelease: (NSNotification*)notification {
//step1: 从named、nameless、wildcard表中查找对应的通知
...
//step2:执行发送,即调用performSelector执行响应方法,从这里可以看出是同步的
[o->observer performSelector: o->selector
withObject: notification];
//step3: 释放资源
RELEASE(notification);
}
总共分为查找通知、发送、释放资源三个流程:
查找通知:
- 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 中。
发送通知:
取出其中 的observer 节点(包含了观察者对象和 selector)通过performSelector:
逐一调用sel
,这是个同步操作
释放资源:
释放notification
对象
总结一下流程:
从三个存储容器中:named
、nameless
、wildcard
去查找对应的Observation
对象,然后通过performSelector:
逐一调用响应方法,这就完成了发送流程
删除通知
调用removeObserver:
方法移除通知
- (void) removeObserver: (id)observer {
if (observer == nil)
return;
[self removeObserver: observer name: nil object: nil];
}
- (void) removeObserver: (id)observer
name: (NSString*)name
object: (id)object {
// 当其要移除的信息都为空时,直接返回
if (name == nil && object == nil && observer == nil)
return;
lockNCTable(TABLE);
// name和object都为nil,就在wildcard链表里删除对应observer的注册信息
if (name == nil && object == nil) {
WILDCARD = listPurge(WILDCARD, observer);
}
// name为空时
if (name == nil) {
GSIMapEnumerator_t e0;
GSIMapNode n0;
// 首先尝试删除为此object对应的所有命名项目
// 在named表中
e0 = GSIMapEnumeratorForMap(NAMED);
n0 = GSIMapEnumeratorNextNode(&e0);
while (n0 != 0) {
GSIMapTable m = (GSIMapTable)n0->value.ptr;
NSString *thisName = (NSString*)n0->key.obj;
n0 = GSIMapEnumeratorNextNode(&e0);
if (object == nil) { // 如果object为空,直接清除named表
// 清空named表
GSIMapEnumerator_t e1 = GSIMapEnumeratorForMap(m);
GSIMapNode n1 = GSIMapEnumeratorNextNode(&e1);
while (n1 != 0) {
GSIMapNode next = GSIMapEnumeratorNextNode(&e1);
purgeMapNode(m, n1, observer);
n1 = next;
}
} else {
// 以object为key找到对应链表,清空该链表
GSIMapNode n1;
n1 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n1 != 0) {
purgeMapNode(m, n1, observer);
}
}
if (m->nodeCount == 0) {
mapFree(TABLE, m);
GSIMapRemoveKey(NAMED, (GSIMapKey)(id)thisName);
}
}
// 开始操作nameless表
if (object == nil) { // object为空时
// 清空nameless表
e0 = GSIMapEnumeratorForMap(NAMELESS);
n0 = GSIMapEnumeratorNextNode(&e0);
while (n0 != 0) {
GSIMapNode next = GSIMapEnumeratorNextNode(&e0);
purgeMapNode(NAMELESS, n0, observer);
n0 = next;
}
} else { // object不为空
// 找到对应的observer链表,清空该链表
n0 = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
if (n0 != 0) {
purgeMapNode(NAMELESS, n0, observer);
}
}
} else { // name不为空
GSIMapTable m;
GSIMapEnumerator_t e0;
GSIMapNode n0;
n0 = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
// 如果没有和这个name相同的key,直接返回
if (n0 == 0) {
unlockNCTable(TABLE);
return; /* Nothing to do. */
}
m = (GSIMapTable)n0->value.ptr; // 找到name作为key对应的数据信息
if (object == nil) {
// 如果object为nil,就清空刚才找到的name对应的数据信息
e0 = GSIMapEnumeratorForMap(m);
n0 = GSIMapEnumeratorNextNode(&e0);
while (n0 != 0) {
GSIMapNode next = GSIMapEnumeratorNextNode(&e0);
purgeMapNode(m, n0, observer);
n0 = next;
}
} else {
// 如果object不为空,清空object对应的链表
n0 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n0 != 0) {
purgeMapNode(m, n0, observer);
}
}
// 因为其中的数据清除完了,所以记得清除named表中的作为key的name
if (m->nodeCount == 0) {
mapFree(TABLE, m);
GSIMapRemoveKey(NAMED, (GSIMapKey)((id)name));
}
}
unlockNCTable(TABLE);
}
name和object都不存在
清空 wildcard 链表
name不存在(不管object存不存在)
遍历 named table,若 object 为 nil,则清空 named table,若 object 不为 nil,则以 object 为 key 找到对应的链表,然后清空链表。在 nameless table 中以 object 为 key 找到对应的 observer 链表,然后清空,若 object 也为 nil,则清空 nameless table。
name存在
在 named table 中以 NotificationName 为 key 找到对应的 table,若 object 为 nil,则清空找到的 table,若 object 不为 nil,则以 object 为 key 在找到的 table 中取出对应的链表,然后清空链表。
异步通知机制实现
异步通知机制通过NSNotificationQueue
的异步发送,从线程的角度看并不是真正的异步发送,或可称为延时发送,它是利用了runloop
的时机来触发的
入队
/*
* 把要发送的通知添加到队列,等待发送
* NSPostingStyle 和 coalesceMask在上面的类结构中有介绍
* modes这个就和runloop有关了,指的是runloop的mode
*/
- (void) enqueueNotification: (NSNotification*)notification
postingStyle: (NSPostingStyle)postingStyle
coalesceMask: (NSUInteger)coalesceMask
forModes: (NSArray*)modes
{
......
// 判断是否需要合并通知
if (coalesceMask != NSNotificationNoCoalescing) {
[self dequeueNotificationsMatching: notification
coalesceMask: coalesceMask];
}
switch (postingStyle) {
case NSPostNow: {
...
// 如果是立马发送,则调用NSNotificationCenter进行发送
[_center postNotification: notification];
break;
}
case NSPostASAP:
// 添加到_asapQueue队列,等待发送
add_to_queue(_asapQueue, notification, modes, _zone);
break;
case NSPostWhenIdle:
// 添加到_idleQueue队列,等待发送
add_to_queue(_idleQueue, notification, modes, _zone);
break;
}
}
- 根据
coalesceMask
参数判断是否合并通知 - 接着根据
postingStyle
参数,判断通知发送的时机,如果不是立即发送则把通知加入到队列中:_asapQueue
、_idleQueue
发送通知
static void notify(NSNotificationCenter *center,
NSNotificationQueueList *list,
NSString *mode, NSZone *zone)
{
......
// 循环遍历发送通知
for (pos = 0; pos < len; pos++)
{
NSNotification *n = (NSNotification*)ptr[pos];
[center postNotification: n];
RELEASE(n);
}
......
}
// 发送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
notify(item->queue->_center,
item->queue->_asapQueue,
mode,
item->queue->_zone);
}
// 发送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
notify(item->queue->_center,
item->queue->_idleQueue,
mode,
item->queue->_zone);
}
runloop
触发某个时机,调用GSPrivateNotifyASAP()
和GSPrivateNotifyIdle()
方法,这两个方法最终都调用了notify()
方法
notify()
所做的事情就是调用NSNotificationCenter
的postNotification:
进行发送通知
主线程响应通知
异步线程发送通知则响应函数也是在异步线程,如果执行UI刷新相关的话就会出问题,那么如何保证在主线程响应通知呢?
其实也是比较常见的问题了,基本上解决方式如下几种:
- 使用
addObserverForName: object: queue: usingBlock
方法注册通知,指定在mainqueue
上响应block
- 在主线程注册一个
machPort
,它是用来做线程通信的,当在异步线程收到通知,然后给machPort
发送消息,这样肯定是在主线程处理的
总结
注册通知:
- 在同步通知中存储是以
name
和object
为维度的,即判定是不是同一个通知要从name
和object
区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都是以这两个为维度的。 - 通过name和object将通知存储划分为三种结构:named表(name存在)、nameless表(name不存在)、wildcard链表(name和object都不存在)
- 在通知的存储过程并没有做去重操作,这也解释了为什么同一个通知注册多次则响应多次
发送通知:
- 首先创建一个数组来存储接收通知的观察者,接着根据name和object作区分来遍历wildcard链表、nameless表、named表
- 遍历所有列表,意味着注册多次通知就会响应多次
- 通过
performSelector:
逐一调用sel
进行发送,这是个同步操作 - 接收通知的线程,和发送通知所处的线程是同一个线程。也就是说如果要在接收通知的时候更新 UI,需要注意发送通知的线程是否为主线程。
删除通知:
-
查找时仍然以
name
和object
为维度的,再加上observer
做区分 -
因为查找时做了这个链表的遍历,所以删除时会把重复的通知全都删除掉
异步通知:
-
依赖
runloop
,所以如果在其他子线程使用NSNotificationQueue
,需要开启runloop -
最终还是通过
NSNotificationCenter
进行发送通知,所以这个角度讲它还是同步的 -
所谓异步,指的是非实时发送而是在合适的时机发送,并没有开启异步线程
页面销毁时不移除通知会崩溃吗?
在观察者对象释放之前,需要调用removeOberver方法将观察者从通知中心移除,否则程序可能会出现崩溃。但从 iOS9 开始,即使不移除观察者对象,程序也不会出现异常。
这是因为在 iOS9 以后,通知中心持有的观察者由 unsafe_unretained 引用变为weak引用。即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空。但是通过 addObserverForName:object: queue:usingBlock: 方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。