通知
一. 通知的基本使用
1. 基本概念
NSNotification 是iOS中一个调度消息通知的类,采用单例模式设计
,在程序中实现传值、回调
等地方应用很广。在iOS中,NSNotification & NSNotificationCenter是使用观察者模式
来实现的用于跨层传递消息。
概要:
-
- 观察者和被观察者都无需知晓对方,只需要通过标记在NSNotificationCenter中找到监听该通知所对应的类,从而调用该类的方法
-
- 并且在NSNotificationCenter中,观察者可以只订阅某一特定的通知,并对其做出相应操作,而不用对某一个类发的所有通知都进行更新操作
-
- NSNotificationCenter对观察者的调用不是随机的,而是遵循注册顺序一一执行的,并且在该线程内是同步的
2. 什么情况下使用通知
观察者模式:定义对象间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
应用场景:
- 对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变。
- 一个对象必须通知其他对象,而它又不需要知道其他对象是什么
3. 如何使用通知
- 在接收参数的地方注册通知,并实现定义方法:
向观察者中心添加观察者(2种方式)
//观察者接收到通知后执行任务的代码在发送通知的线程中执行
addObserver:selector:name:object:
//观察者接收到通知后执行任务的代码在指定的操作队列中执行
addObserverForName:object:queue:usingBlock:
- 创建一个通知
//方法一:不带object信息
NSNotification *notification = [NSNotification notificationWithName:@"MyNotification" object:self];
//方法二:带object信息
NSDictionary *userInfo = @{@"key1": @"value1", @"key2": @"value2"};
NSNotification *notification = [NSNotification notificationWithName:@"MyNotification" object:self userInfo:userInfo];
- 在要传递参数的地方,发送通知给通知中心通知中心
//先创建好通知,直接发送通知
postNotification:
//不用先创建通知,这个方法自己就创建并发送了
postNotificationName:object:
postNotificationName:object:userInfo:
-
观察者接收到消息执行相应的行为
-
在通知中心移除观察者
removeObserver:
removeObserver:name:object:
4. 使用通知需要注意哪些细节
- 通知一定要移除,在dealloc方法里面移除
- 通知有同步通知和异步通知,只不过我们同步通知用的比较多
- 不能用-(instancetype)init初始化一个通知(因为没有这个方法)。通知有自己的两种初始化方法。
同步通知和异步通知:
同步通知:
- (IBAction)sendSyncNotification:(id)sender {
// 发送同步通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"SyncNotification" object:nil];
}
异步通知 :
- (IBAction)sendAsyncNotification:(id)sender {
// 在后台线程发送异步通知
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行耗时的操作
sleep(2);
// 发送异步通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"AsyncNotification" object:nil];
});
}
二. 通知的实现原理
- 概述:首先,信息的传递是依靠(NSNotification),也就是说,通知就是信息(执行的方法,观察者本身(self),参数)的包装。通知中心(NSNotificationCenter)是一个单例,向通知中心注册观察者,也就是说,这个通知中心有个集合,这个集合存放着观察者。那么这个集合是什么样的数据类型?可以这么思考:发送通知需要name参数,添加观察者也有一个name参数,这个两个name一样的时候,当发送通知的时候,观察者对象就能接受到信息,执行对应的操作。那么这个集合很容易想到就是NSDictionary!key就是name,value就是NSArray(存放数据模型),里面存放观察者对象。如下图
当发送通知时,在通知中心的字典,根据name找到value,这个value就是一个数组,数组里面存放数据模型(observer,SEL)。即可执行对应的行为。
- 自定义实现通知功能
首先创建一个自定义文件NotificationCenter,继承自NSObject,用做自定义通知的类,因为通知是可以实现多对多关系的,所以我们在这个类中还需要定义一个可变的字典属性,用来存储注册的通知。又因为注册的通知数据需要一直保存下来,所以我们使用单例来完成这一操作,保证我们在想要访问已经注册的通知的时候,其数据是存在的。
@interface NotificationCenter ()
// 因为通知是多对多的关系,所以这里定义一个可变字典用来存储对应关系
@property (nonatomic, strong) NSMutableDictionary *classDictionary;
@end
@implementation NotificationCenter
// 实现默认的通知中心,是个单例,防止其自动销毁
+ (instancetype)defaultCenter {
// 定义一个锁
static dispatch_once_t onceToken;
// 创建通知中心的单例,同时初始化其中数据
static NotificationCenter *notificationCenter = nil;
dispatch_once(&onceToken, ^{
notificationCenter = [NotificationCenter new];
notificationCenter.classDictionary = [NSMutableDictionary dictionary];
});
return notificationCenter;
}
@end
因为我们使用通知的目的就是为了传递参数,供别的类来使用,所以我们这里再定义一个专门保存通知的类ZJQNotification,其中包含通知的必要信息:通知的名称、通知传递的参数信息、以及一个id类型的object。因为这是需要给外界透露的接口,外界不能对其进行写操作,所以为只读(readOnly)属性,除了这三个参数,当然还得需要一个快速的初始化类的方法:
// 通知类,用来保存通知及其参数
@interface ZJQNotification : NSObject
// 对外有三个只读属性
@property (readonly, copy) NSNotificationName name; // 通知名
@property (nullable, readonly, copy) NSDictionary *userInfo; // 参数信息
@property (nullable, readonly, retain) id object; // 接收通知的对象
// 快速初始化类参数的方法
- (instancetype)initWithName:(NSString *)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo;
@end
该通知类中的三个参数是对外只读的,内部可以进行修改,所以我们在内部重写属性,实现可读可写,同时实现该类快速初始化方法:
@interface ZJQNotification()
// 在内部可修改这三个属性
@property (nonatomic, copy) NSString *name; // 通知名
@property (nonatomic, copy) NSDictionary *userInfo; // 参数信息
@property (nonatomic, retain) id object; // 接收通知的对象
@end
// 通知类,用来保存通知及其参数
@implementation ZJQNotification
// 快速初始化类参数的方法
- (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo {
ZJQNotification *notification = [ZJQNotification new];
notification.name = name;
notification.object = object;
notification.userInfo = userInfo;
return notification;
}
@end
接着我们在之前定义的通知中心ZJQNotificationCenter中再定义通知常用的对外开放的接口,注册通知、发送通知、删除通知:
// 通知中心
@interface ZJQNotificationCenter : NSObject
// 默认的通知中心
+ (instancetype)defaultCenter;
// 添加通知中心
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;
// 发送通知
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSString *)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;
@end
接着就一个一个实现,首先是添加通知的逻辑,我们在添加、调用通知的时候,需要知道三个必要信息,一个是创建该通知的实例类,一个是通知要调用的自定义方法,以及一个object。所以我们需要将这三个信息保存起来,一起保存到刚刚我们创建的通知中心单例的classDictionary属性中,为了方便我们后续的查找,所以该属性的key我们使用注册通知时通知的名称标识,因为一个通知可能对应多个类,所以这里我们value使用一个数组,该数组中的每个变量又是一个字典,其中的内容就是上边所说的三个参数:
// 添加通知中心
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject {
// 以通知名为key来设置value并保存在通知中心
// 从通知中心获取该通知名的所有注册信息
NSMutableArray *array = self.classDictionary[aName];
// 如果通知中心没有储存过该通知名的信息,就新建
if (!array) {
array = [NSMutableArray array];
}
// 向数组中添加传递过来的信息
[array addObject:@{@"class": observer, @"selector": NSStringFromSelector(aSelector), @"object": anObject ? : [NSNull null]}];
// 将更新过的数组重新添加到通知中心
[self.classDictionary setObject:array forKey:aName];
}
发送通知的逻辑,其实就是通过发送过来的通知名标识aName在classDictionary属性中查找对应的通知信息,然后依次使用objc_msgSend发送消息,从而达到传值的目的,这里我们用系统封装好的NSInvocation类进行调用:
// 发送通知
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject {
[self postNotificationName:aName object:anObject userInfo:nil];
}
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo {
// 通过发送通知的通知名,找到在通知中心保存的该通知名的所有注册的类的信息
NSMutableArray *array = self.classDictionary[aName];
// 通过获取到的信息中的方法名和类信息,来逐一使用msgSend发送消息给目标类
for (NSDictionary *mapDictionary in array) {
// 当mapDictionary中的object与anObject一致或者接收者为null时,才调用方法,确保信息无误不会发错
if ([mapDictionary[@"object"] isEqual:anObject] || [mapDictionary[@"object"] isKindOfClass:[NSNull class]]) {
// NSInvocation;用来包装方法和对应的对象,它可以存储方法的名称,对应的对象,对应的参数,
/*
NSMethodSignature:签名:再创建NSMethodSignature的时候,必须传递一个签名对象,签名对象的作用:用于获取参数的个数和方法的返回值
*/
// 创建签名对象的时候不是使用NSMethodSignature这个类创建,而是方法属于谁就用谁来创建,创建当前访问信息的class类中的selector方法的签名
NSMethodSignature *signature = [[mapDictionary[@"class"] class] instanceMethodSignatureForSelector:NSSelectorFromString(mapDictionary[@"selector"])];
// 1、通过创建的方法签名,创建NSInvocation对象
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
// 给NSInvocation对象设置实现该selector方法的类信息
invocation.target = mapDictionary[@"class"];
// 给NSInvocation对象设置调用的方法的信息
invocation.selector = NSSelectorFromString(mapDictionary[@"selector"]);
// 包装要使用通知传递的信息,通过消息转发机制来实现跨界面传值
// 注意:设置参数的索引时不能从0开始,因为0已经被self占用,1已经被_cmd占用
ZJQNotification *notification = [[ZJQNotification alloc] initWithName:aName object:anObject userInfo:aUserInfo];
[invocation setArgument:¬ification atIndex:2];
/* 第一个参数:需要给指定方法传递的值
第一个参数需要接收一个指针,也就是传递值的时候需要传递地址 */
// 第二个参数:需要给指定方法的第几个参数传值
// 2、调用NSInvocation对象的invoke方法
// 只要调用invocation的invoke方法,就代表需要执行NSInvocation对象中制定对象的指定方法,并且传递指定的参数
[invocation invoke];
}
}
}
移除通知时,通过给定的通知的信息,在classDictionary中删除对应的数据即可:
// 移除通知
// 找到对应的类,在classDictionary中删除即可
// 全部移除
- (void)removeObserver:(id)observer {
// 创建一个临时字典,存放删除完了的数据,最后用这个字典更新classDictionary数据
NSMutableDictionary *tempDictionary = [NSMutableDictionary dictionary];
// 枚举遍历
[self.classDictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSMutableArray *tempArray = [obj mutableCopy];
for (NSDictionary * mapDictionary in obj) {
// 判断该类是不是observer的类,是就删除
if ([mapDictionary[@"class"] isKindOfClass:[observer class]]) {
[tempArray removeObject:mapDictionary];
}
}
// 删除完了,添加到tempDictionary中
[tempDictionary setObject:tempArray forKey:key];
}];
// 更新classDictionary数据
self.classDictionary = tempDictionary;
}
// 根据通知名移除
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject {
// 获取该通知名注册的所有信息
NSMutableArray *array = self.classDictionary[aName];
NSMutableArray *tempArray = [array mutableCopy];
for (NSDictionary *mapDictionary in array) {
// 判断该类是不是observer的类
if ([mapDictionary[@"class"] isKindOfClass:[observer class]]) {
// 如果该通知的接收方相等或者接收方不存在,再删除,确保不会误删
if ([mapDictionary[@"object"] isEqual:anObject] || !anObject) {
[tempArray removeObject:mapDictionary];
}
}
}
// 更新classDictionary数据
[self.classDictionary setValue:tempArray forKey:aName];
}
通知原理
通知机制的核心是一个与线程关联的单例对象叫通知中心(NSNotificationCenter)。通知中心发送通知给观察者是同步的,也可以同通知队列(NSNotificationQueue)异步发送通知。
因为苹果的通知源码没有开源,所以我们看看GNUStep的源码。
属于结构
单例类
从我们之前使用通知的流程和代码来看,通知其实就是一个单例,方便随时访问。
static NSNotificationCenter *default_center = nil;
+ (NSNotificationCenter*) defaultCenter
{
return default_center;
}
NSNotificationCenter : 消息中心
这个单例类中主要定义了两个表,一个存储所有注册通知信息的表的结构体,一个保存单个注册信息的节点结构体。
typedef struct NCTbl {
Observation *wildcard; // 添加观察者时既没有传入 NotificationName ,又没有传入object,就会加在这个链表上,它里边的观察者可以接收所有的系统通知
GSIMapTable nameless; // 添加观察者时没有传入 NotificationName 的表
GSIMapTable named; // 添加观察者时传入了 NotificationName 的表
} NCTable
保存了观察者的信息:
typedef struct Obs {
id observer; // 观察者对象
SEL selector; // 方法信息
struct Obs *next; // 指向下一个节点
int retained; /* Retain count for structure. */
struct NCTbl *link; /* Pointer back to chunk table */
} Observation;
以及一些宏定义:
#define TABLE ((NCTable*)_table)
#define WILDCARD (TABLE->wildcard)
#define NAMELESS (TABLE->nameless)
#define NAMED (TABLE->named)
#define LOCKCOUNT (TABLE->lockCount)
named表
在 named 表中,NotifcationName 作为表的 key,因为我们在注册观察者的时候是可以传入一个参数 object 用于只监听指定该对象发出的通知,并且一个通知可以添加多个观察者,所以还需要一张表来保存 object 和 Observer 的对应关系。这张表的 key、Value 分别是以 object 为 Key,Observer 为 value。用了链表这种数据结构实现保存多个观察者的情况。
在实际开发过程中 object 参数我们经常传 nil,这时候系统会根据 nil 自动生成一个 key,相当于这个 key 对应的 value(链表)保存的就是当前通知传入了 NotificationName 没有传入 object 的所有观察者。
nameless表
上边说了 named 表,那么 nameless 表就不难想象了,他注册时没有 NotificationName ,即没有了最外边一层键值对的约束了,其中就只有 object 和 Observation 所对应的键值对结构了:
wildcard表
这个表既没有 NotificationName 也没有 object 了,所以他就会在 nameless基础上在脱去一层键值对,那么它就只剩下一个链表了,该练表存储了可以接收所有通知的类的信息
添加观察者
使用方法addObserver:selector:name:object添加观察者,根据 GNUStep 的源码分析:
- (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,存储这次注册的信息
o = obsNew(TABLE, selector, observer);
// 如果有name
if (name) {
// 在named表中 以name为key寻找value
n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
// named表中没有找到对应的value
if (n == 0) {
// 新建一个表
m = mapNew(TABLE);
// 由于这是对给定名称的首次观察,因此我们对该名称进行了复制,以便在map中无法对其进行更改(来自GNUStep的注释)
name = [name copyWithZone: NSDefaultMallocZone()];
// 新建表作为name的value添加在named表中
GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
GS_CONSUMED(name)
} else { //named表中有对应的value
// 取出对应的value
m = (GSIMapTable)n->value.ptr;
}
// 将observation添加到正确object的列表中
// 获取添加完后name对应的value的object对应的链表
n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
// n是object的value
if (n == 0) { // 如果object对应value没有数据
o->next = ENDOBS;
// 将o作为object的value链表的头结点插入
GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
} else { // 如果有object对应的value那么就直接添加到原练表的尾部
// 在链表尾部加入o
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
// 这个else if 就是没有name有object的Observation,对object进行的操作相同,
} else if (object) {
// 直接获取object对应的value链表
n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
if (n == 0) { // 这个对应链表如果没有数据
o->next = ENDOBS;
// 将该observation作为头节点插入
GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
} else { // 有数据,将obsevation直接插在原链表的后面
list = (Observation*)n->value.ptr;
o->next = list->next;
list->next = o;
}
} else {
// 既没有name又没有object,就加在WILDCARD链表中
o->next = WILDCARD;
WILDCARD = o;
}
// 解锁
unlockNCTable(TABLE);
}
流程总结:
-
- 如果提供了name参数
- 在named表中根据name查找对应的GSIMapTable。如果没有找到,则创建一个新的GSIMapTable并将其添加到NAMED表中。
- 将新创建的Observation对象添加到GSIMapTable中object对应的链表的末尾
- 如果提供了name参数
-
- 如果没有提供name参数,但提供了object参数:
- 在nameless表中查找object对应的链表。如果没有找到,则将新创建的Observation对象作为头节点插入到nameless表中
- 如果找到了object对应的链表,则将新创建的Obervation对象添加到链表的末尾。
- 如果没有提供name参数,但提供了object参数:
-
- 如果既没有提供name也没有提供object,则将新创建的Observation对象添加到wildcard链表的头部
发送通知
使用方法postNotification:
, postNotificationName:object:userInfo
或者postNotificationName:object:
发送通知,后者默认userInfo
为nil,同样使用GNUStep
源码进行分析:
- (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 *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);
}
流程总结
-
- 首先会创建一个数组,observerArray用来保存需要通知的observer。
-
- 遍历wildcard链表,将observer添加到observerArray数组中
-
- 若存在object,在nameless table中找到以object为key到链表,然后遍历找到的链表,将observer添加到observerArray数组中
-
- 若存在NotificationName,在named table中以NotificationName为key找到对应的table,然后再在找到的他变了中以object为key找到对应的链表,遍历链表,将observer添加到observerArray数组中。结果object不为nil,则以nil为key找到对应的链表,遍历链表,将observer添加到observerArray数组中。
-
- 至此所有关于当前通知的observer(wildcard + nameless + named)都已经加入到了数组observerArray中。遍历observerArray数组,取出其中的observer节点(包含了观察者对象和selector)其中调用观察者的方法,调用形式如下:
[o->observer performSelector: o->selector withObject: notification];
移除通知
- (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);
}
流程总结
-
- 若NotificationName和object都为nil,则清空wildcard链表。
-
- 若NotificationName为nil,遍历named table,若object为nil,则清空named table,若object不为nil,则以object为key找到对应的链表,然后清空链表。在nameless table中以object为key找到对应的observer链表,然后清空,若object也为nil,则清空nameless table
-
- 若NotificationName不为nil,在name table中以NotificationName为key找到对应的table,若object为nil,则清空找到的table,若object不为nil,则以object为key在找到的table中取出对应的链表,然后清空链表
面试题
1. 通知的发送是同步的还是异步的?发送消息与接收消息的线程是同一个线程吗?
答:
通知中心发送通知给观察者是同步的,也可以同通知队列(NSNotificationQueue)异步发送通知。
在抛出通知以后,观察者在通知事件处理完成以后(可以通过休眠3秒来测试),抛出者才会往下继续执行,也就是说这个过程默认是同步的;当发送通知时,通知中心会一直等到所有的observer都收到并且处理了通知才会返回到poster。
接收通知的线程,和发送通知所处的线程是同一个线程。也就是说如果要在接收通知的时候更新UI,需要注意发送通知的线程是否为主线程。
2. 如何使用异步发送通知?
- 让通知事件处理方法在子线程中执行:
- (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);
});
}
- 可以通过
NSNotificationQueue
的enqueueNotification: postingStyle:
和enqueueNotification: postingStyle: coalesceMask: forModes:
方法将通告放入队列,实现异步发送,在把通告放入队列之后,这些方法会立即将控制权返回给调用对象。
- (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);
}
3. NSNotificationQueue和runloop的关系?
postingStyle参数就是定义通知调用和runloop状态之间的关系。
该参数的三个可选参数:
-
- NSPostWhenIdle : 通知回调方法是等待到当下线程runloop进入等待状态才会调用。( 表示在当前 runloop 空闲时分发通知)
-
- NSPostASAP:通知回调方法是等待到当下线程runloop开始接收事件源的时候就会调用。(表示尽快分发通知,通常是在当前 runloop 中立即分发。)
-
- NSPostNow:其实和直接用默认的通知中心添加通知是一样的,通知马上调用回调方法。(这种模式下,通知会立即被分发,不需要等待 runloop 的任何事件触发。)
4. 页面销毁时不移除通知会崩溃吗?
- 在观察者对象释放之前,需要调用removeOberver方法将观察者从通知中心移除,否则程序可能会出现崩溃。但从 iOS9 开始,即使不移除观察者对象,程序也不会出现异常。
- 这是因为在iOS9以后,通知中心持有的观察者由
unsafe_unretained
引用变为weak
引用。即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空。但是通过addObserverForName:object:queue:usingBlock:方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。
5. 多次添加同一个通知会是什么结果?多次移除通知呢?
- 多次添加同一个通知,每次添加都会成功注册一个新的观察者,通知发送时观察者方法会调用多次
- 多次移除,是安全的。第一次调用会成功移除观察者。后续的移除调用则会什么也不做,因为观察者已经不存在了。
6. 下面的方法会不会接收到通知?
不会,上文介绍NSNotificationCenter时介绍了center的结构。
- 注册通知在添加observer时,路径为TestNotification -> @1 -> self
- 发送通知在查找observer时,路径为TestNotification -> nil -> observer list
7. object是干嘛的?是不是可以用来传值?
object是用来过滤Notification的,只接收指定的sender所发的Notification,传值请用userInfo,而不是object。