内容简介:推荐另一篇文章:
推荐另一篇文章: 透彻理解 KVO 观察者模式(附基于runtime实现代码)
写在前面
NSNotificationCenter
这个东西作为iOS工程师想必都不陌生,但是有人可能连参数的意义都没搞明白,写这篇文章的目的不止是为了让不会用的人会用,更是为了让会用的人理解得更透彻。本篇文章主要是梳理 NSNotificationCenter
的特性和值得注意的地方,并且在后面结合对其特性的分析手动利用代码来实现它。
一、分析
1、 基本使用方法
直接进入 NSNotification
文件。
@property (class, readonly, strong) NSNotificationCenter *defaultCenter;
该属性是获取 NSNotificationCenter
唯一单例,它就是一个消息分发中心,通过使用这个唯一的实例我们进行 添加通知、发送通知、移除通知
。
(1) 添加通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondsToNotification:) name:@"test0" object:_obj0];
_obj0
是创建的一个实例,这里暂时不讨论 object
参数的用法。 Observer
即为响应者无需多说; selector
即为一个响应通知的方法,需要 SEL
类型; name
是一个标识,通知中心主要是通过它来实现消息的精确分发(当然object也有定位作用)。
(2) 发送通知
//便利方法[[NSNotificationCenter defaultCenter] postNotificationName:@"test0" object:_obj0 userInfo:@{@"key":@"_obj0"}];//使用NSNotificationNSNotification *notification = [[NSNotification alloc] initWithName:@"test0" object:_obj2 userInfo:@{@"key":@"_obj2"}]; [[NSNotificationCenter defaultCenter] postNotification:notification];
发送通知和添加通知对应,需要 name、object
参数,这里多了一个 userInfo
,该参数可以把你需要携带的数据发送给该通知的响应者。
其实我们可以很轻易的想到,便利发送通知方法不过是对于使用 NSNotification
发送通知的一个语法糖, NSNotification
才是消息体。
(3) 移除通知
//移除该响应者的全部通知[[NSNotificationCenter defaultCenter] removeObserver:self];//移除该响应者 name==@"test0" 的全部通知[[NSNotificationCenter defaultCenter] removeObserver:self name:@"test0" object:nil];//移除该响应者 name==@"test0" 且 object==_obj0 的全部通知[[NSNotificationCenter defaultCenter] removeObserver:self name:@"test0" object:_obj0];
移除通知这里有点讲究,从上至下越来越 “精准” 。
在合理的位置移除通知是至关重要的:
1、让不希望继续接受通知的响应者失去对该通知的响应;
2、避免重复添加相同通知(响应者的内存为同一块的时候);
3、通知中心对响应者 observer
是使用 unsafe_unretained
修饰,当响应者释放会出现野指针,向野指针发送消息造成崩溃;在iOS 9(更新的系统版本有待考证)之后,苹果对其做了优化,会在响应者调用 dealloc
方法的时候执行 removeObserver:
方法。
注意:在后文会详细分析该问题。
当然,常规的业务场景一般是在该响应者释放的时候移除。
- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; }
(4) 响应通知
- (void)respondsToNotification:(NSNotification *)noti { id obj = noti.object; NSDictionary *dic = noti.userInfo; NSLog(@"\n- self:%@ \n- obj:%@ \n- notificationInfo:%@", self, obj, dic); }
响应通知的时候会将 NSNotification
消息体传递过来,如代码所示。
2、object:(nullable id)anObject参数
-
添加通知时,若指定了
object
参数,那么该响应者只会接收 发送通知 时object
参数指定为同一实例的通知。 -
发送通知时,若指定了
object
参数,并不会影响 添加通知 时没有指定object
参数的响应者接收通知。
如果感觉有点绕,看如下代码便知。
//添加通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondsToNotification:) name:@"test0" object:nil];//发送通知 [[NSNotificationCenter defaultCenter] postNotificationName:@"test0" object:_obj0];//由于添加通知时,object==nil,所以该响应者仍然能接收到该通知。
//添加通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondsToNotification:) name:@"test0" object: _obj0];//发送通知 [[NSNotificationCenter defaultCenter] postNotificationName:@"test0" object:nil];//由于添加通知时,指定了object==_obj0,而发送通知时,object==nil,所以无法接收到通知//(只有当object==_obj0才能接收到通知)。
3、通知线程问题
我们进入全局队列发送这个通知
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"发送通知 currentThread : %@", [NSThread currentThread]); [[NSNotificationCenter defaultCenter] postNotificationName:@"test0" object:nil]; });
在接收通知的地方将线程打印出来
发送通知 currentThread : {number = 3, name = (null)} 响应通知 currentThread : {number = 3, name = (null)}
结论:通知发送线程和通知接收线程是一致的。
由此看来,如果当我们不是百分之百确认通知的发送队列是在主队列中时,我们最好加上如下代码从而对我们的UI进行处理。
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) { //UI处理} else { dispatch_async(dispatch_get_main_queue(), ^{ //UI处理 }); }
4、是否需要移除通知?
以下代码模拟重复添加通知的情况,所以如果可能会重复添加通知,我们都应该做好相应的处理。
for (int i = 0; i < 3; i++) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondsToNotification:) name:@"test0" object:nil]; }//该代码导致的结果是,响应通知回调会走三次。
可能有人会问,为什么系统库没有做个重复添加的判断?当然,这可能是为了让我们更灵活的运用,也可能是对时间复杂度的一种妥协吧
有过比较长开发经验的同学应该都有过,没有及时的移除通知而导致意外崩溃的情况。前面也说过,通知中心对响应者 observer
是使用 unsafe_unretained
修饰,当响应者释放会出现野指针,如果向野指针发送消息会造成崩溃。在 iOS9 系统之后, [NSNotificationCenter defaultCenter]
会在响应者 observer
调用 -dealloc
方法的时候执行 -removeObserver:
方法。
在官方文档中有这样一段话:
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.
动手做个小实验:
新建一个 NSNotificationCenter
的分类,代码如下:
@implementation NSNotificationCenter (YB)+ (void)load { Method origin = class_getInstanceMethod([self class], @selector(removeObserver:)); Method current = class_getInstanceMethod([self class], @selector(_removeObserver:)); method_exchangeImplementations(origin, current); } - (void)_removeObserver:(id)observer { NSLog(@"调用移除通知方法: %@", observer);// [self _removeObserver:observer];}@end
然后新建一个类正常的使用通知,但是请不要手动在 -dealloc
中释放通知(我们要做实验)。然后我们释放掉这个类(可以使用控制器present、dismiss)。
调用移除通知方法: <test_vc: nbsp="" 0x7f9a0a4d9240=""></test_vc:>
神奇的现象发生了,通过比较内存地址, [NSNotificationCenter defaultCenter]
确实是调用了 removeObserver :
方法移除对应响应者的通知监听。
注意上面的代码中,我将 [self _removeObserver:observer];
注释掉了,意味着该方法已经被我截取了,我们再向该“移除通知未遂”的响应者 observer
发送通知,直接崩溃。当去除注释,正常运行,无需手动移除。
结论:如果iOS支持版本在 iOS9 以上,多数情况理论上可以不用移除通知,但是由于历史遗留、开发者习惯等因素,看个人喜好了
二、代码实现
心血来潮,看着 NSNotification.h
的API和本着对其的理解,决定着手实现一波。
其实仔细一想,通知的功能类似于一个路由,它的基本实现思路并不复杂。我们要做的无非是“添加”、“发送”、“移除”三件事。
但是在具体实现中,还是有些比较麻烦的地方,下面具体叙述(最好下载demo便于理解)。
1、添加通知
首先,创建了一个 YBNotificationCenter
类,属性如下:
@property (class, strong) YBNotificationCenter *defaultCenter;@property (strong) NSMutableDictionary *observersDic;
defaultCenter
类属性不用说,它是唯一单例(具体实现看代码); observersDic
即为用来存储添加通知相关信息的字典。
然后创建了一个 YBObserverInfoModel
类,属性如下:
@property (weak) id observer;@property (strong) id observer_strong;@property (strong) NSString *observerId;@property (assign) SEL selector;@property (weak) id object;@property (copy) NSString *name;@property (strong) NSOperationQueue *queue;@property (copy) void(^block)(YBNotification *noti);
该类就是响应者信息存储模型类,也就是会放在上面 observersDic
字典内的元素。先回忆一下,当我们使用 - (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;
或 - (id
方法时,这些配置的变量是不是在 YBObserverInfoModel
都有体现呢?
是的,添加通知的操作不过就是将我们需要配置的变量统统存储起来,但是注意几点:一是对 observer
和 object
不能强持有,否则其无法正常释放;二是对 name
属性最好使用 copy
修饰,保证其不会受外部干扰;三是 observer_strong
属性是在使用代码块回调的那个添加通知方法时,需要使用到的强引用属性;四是 observerId
属性会比较陌生,它的作用大家可以先不管,之后会有用处。
添加通知核心代码
- (void)addObserverInfo:(YBObserverInfoModel *)observerInfo { //添加进observersDic NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic; @synchronized(observersDic) { NSString *key = (observerInfo.name && [observerInfo.name isKindOfClass:NSString.class]) ? observerInfo.name : key_observersDic_noContent; if ([observersDic objectForKey:key]) { NSMutableArray *tempArr = [observersDic objectForKey:key]; [tempArr addObject:observerInfo]; } else { NSMutableArray *tempArr = [NSMutableArray array]; [tempArr addObject:observerInfo]; [observersDic setObject:tempArr forKey:key]; } } }
我们传入一个配置好的 YBObserverInfoModel
模型进入方法,构建一个树形结构,用传入的 name
作为 key
(如果 name
为空使用 key_observersDic_noContent
常量代替),把所有使用相同 name
的通知放进同一个数组作为 value
,并且添加了线程锁保证 observersDic
数据读写安全。
这么做的理由:在通知的整个功能体系中,“添加”、“发送”、“移除”哪一步对效率的要求最高?毫无疑问是“发送”的时候,我们通常使用 - (void)postNotificationName:(NSString *)aName object:(nullable id)anObject
方法发送通知, aName
参数将是我们找到对应通知的第一匹配点。如果我们将其它参数作为 observersDic
的 key
,我们发送通知的时候不得不遍历整个 observersDic
;而如上代码实现,发送通知的时候,直接就能通过 key
直接找到对应的通知信息了,有效降低了时间复杂度。
使用代码块回调通知方法的实现
- (id <nsobject> )addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(YBNotification * _Nonnull))block { if (!block) { return nil; } YBObserverInfoModel *observerInfo = [YBObserverInfoModel new]; observerInfo.object = obj; observerInfo.name = name; observerInfo.queue = queue; observerInfo.block = block; NSObject *observer = [NSObject new]; observerInfo.observer_strong = observer; observerInfo.observerId = [NSString stringWithFormat:@"%@", observer]; [self addObserverInfo:observerInfo]; return observer; } </nsobject>
这里有个地方需要提出来谈谈,在使用系统的这个方法的时候,一经实验就能发现,不管我们强引用或者弱引用这个返回值 id
时,都能在业务类dealloc释放的时候有效的移除该通知。
由于使用该方法添加通知的时候不会传入 observer
参数,这里创建了一个 observer
,如果这里使用 observerInfo.observer = observer;
,而业务类没有强引用这个返回值 observer
,它将会自然释放。所以,这里做了一个特殊处理,让 observerInfo
实例强持有 observer
。
值得注意的是,外部如果强引用返回的 id
类型的 observer
,会造成 observer
无法及时的释放,但是这点内存我认为还是可以接受的,当然业务类使用弱引用该 observer
是最好的选择。
2、发送通知
和系统通知一样,同样创建了一个类 YBNotification
发送通知消息体,属性就我们熟悉的几个:
@property (copy) NSString *name;@property (weak) id object;@property (copy) NSDictionary *userInfo;
然后将
两个协议实现一下就好了,具体看demo。
发送通知核心代码
- (void)postNotification:(YBNotification *)notification { if (!notification) { return; } NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic; NSMutableArray *tempArr = [observersDic objectForKey:notification.name]; if (tempArr) { [tempArr enumerateObjectsUsingBlock:^(YBObserverInfoModel *obj, NSUInteger idx, BOOL * _Nonnull stop) { if (obj.block) { if (obj.queue) { NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ obj.block(notification); }]; NSOperationQueue *queue = obj.queue; [queue addOperation:operation]; } else { obj.block(notification); } } else { if (!obj.object || obj.object == notification.object) {#pragma clang diagnostic push#pragma clang diagnostic ignored "-Warc-performSelector-leaks" obj.observer?[obj.observer performSelector:obj.selector withObject:notification]:nil;#pragma clang diagnostic pop } } }]; } }
发送通知相对简单,只需要分清是使用代码块回调,还是通过执行SEL回调。在使用代码块回调时,如果传入了队列 queue
,就让该代码块在该队列中执行,否则正常执行。
!obj.object || obj.object == notification.object
if语句中这个判断值得注意。
3、移除通知
移除通知本身简单,有些麻烦的是自动移除。先贴上移除代码:
- (void)removeObserverId:(NSString *)observerId name:(NSString *)aName object:(id)anObject { if (!observerId) { return; } NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic; @synchronized(observersDic) { if (aName && [aName isKindOfClass:[NSString class]]) { NSMutableArray *tempArr = [observersDic objectForKey:[aName mutableCopy]]; [self array_removeObserverId:observerId object:anObject array:tempArr]; } else { [observersDic enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSMutableArray *obj, BOOL * _Nonnull stop) { [self array_removeObserverId:observerId object:anObject array:obj]; }]; } } } - (void)array_removeObserverId:(NSString *)observerId object:(id)anObject array:(NSMutableArray *)array { @autoreleasepool { [array.copy enumerateObjectsUsingBlock:^(YBObserverInfoModel *obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj.observerId isEqualToString:observerId] && (!anObject || anObject == obj.object)) { [array removeObject:obj]; return; } }]; } }
所有移除通知的方法,最终落脚点都是在这里。
上面方法中,如果aName不是合理的,就需要遍历 observersDic
移除对应的通知;如果aName是合理的,就直接查找对应的数组移除内容。
使用 observerId
属性移除通知,而不用 observer
响应者来直接比较移除:
还记得添加通知时 YBObserverInfoModel
类的 @property (strong) NSString *observerId;
属性么?在添加通知的时候,我将响应者的地址信息作为该属性的值(保证其唯一性):
observerInfo.observerId = [NSString stringWithFormat:@"%@", observer];
然后在移除的时候通过比较进行相应的操作。
实现自动移除通知(解释为何使用observerId移除通知而不用observer)
实现自动移除通知,思路是在响应者 observer
走 dealloc
的时候移除对应的通知,难点就是在ARC中是不允许对 dealloc
做继承和交换方法等操作的,所以我使用了一个缓兵之计——动态给 observer
添加一个属性,我们监听这个属性的 dealloc
方法移除对应的通知,代码如下:
- (void)addObserverInfo:(YBObserverInfoModel *)observerInfo { //为observer关联一个释放监听器 id resultObserver = observerInfo.observer?observerInfo.observer:observerInfo.observer_strong; if (!resultObserver) { return; } YBObserverMonitor *monitor = [YBObserverMonitor new]; monitor.observerId = observerInfo.observerId; const char *keyOfmonitor = [[NSString stringWithFormat:@"%@", monitor] UTF8String]; objc_setAssociatedObject(resultObserver, keyOfmonitor, monitor, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //添加进observersDic NSMutableDictionary *observersDic = YBNotificationCenter.defaultCenter.observersDic; @synchronized(observersDic) { NSString *key = (observerInfo.name && [observerInfo.name isKindOfClass:NSString.class]) ? observerInfo.name : key_observersDic_noContent; if ([observersDic objectForKey:key]) { NSMutableArray *tempArr = [observersDic objectForKey:key]; [tempArr addObject:observerInfo]; } else { NSMutableArray *tempArr = [NSMutableArray array]; [tempArr addObject:observerInfo]; [observersDic setObject:tempArr forKey:key]; } } }
只不过在添加通知到 observersDic
之前,添加一个 monitor
实例,使用 objc_setAssociatedObject
动态关联方法给 resultObserver
添加一个强引用的属性,注意 objc_setAssociatedObject
方法的第二个参数必须保证其唯一性,因为同一个响应者可能添加多个通知。
好了,现在基本工作都完成了,只需要在这个 YBObserverMonitor
方法中做简单的移除逻辑就OK了,代码如下:
//监听响应者释放类@interface YBObserverMonitor : NSObject@property (strong) NSString *observerId;@end@implementation YBObserverMonitor- (void)dealloc { NSLog(@"%@ dealloc", self); [YBNotificationCenter.defaultCenter removeObserverId:self.observerId]; }@end
变量的释放顺序各种不确定,可能走 YBObserverMonitor
的 dealloc
时, observer
响应者对象已经释放了,所以不直接使用 observer
响应者对象对比做释放操作。
写在后面
关于实现部分,虽然我做了个大致的测试,可能还是会存在一些潜在的问题,希望各位大佬不惜笔墨点拨一番
附: NSNotification 代码实现Demo地址
作者:indulge_in
链接:https://www.jianshu.com/p/e3a38b21420c
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
应用随机过程教程及在算法和智能计算中的随机模型
龚光鲁 / 清华大学出版社 / 2004-3 / 42.00元
应用随机过程教程及在算法和智能计算中的随机模型,ISBN:9787302069485,作者:龚光鲁,钱敏平著一起来看看 《应用随机过程教程及在算法和智能计算中的随机模型》 这本书的介绍吧!
XML 在线格式化
在线 XML 格式化压缩工具
Markdown 在线编辑器
Markdown 在线编辑器