透彻理解 NSNotificationCenter 通知(附实现代码)

栏目: IOS · 发布时间: 5年前

内容简介:推荐另一篇文章:

推荐另一篇文章: 透彻理解 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 代码实现Demo地址

心血来潮,看着 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 )addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(YBNotification *note))block; 方法时,这些配置的变量是不是在 YBObserverInfoModel 都有体现呢?

是的,添加通知的操作不过就是将我们需要配置的变量统统存储起来,但是注意几点:一是对 observerobject 不能强持有,否则其无法正常释放;二是对 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 参数将是我们找到对应通知的第一匹配点。如果我们将其它参数作为 observersDickey ,我们发送通知的时候不得不遍历整个 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)

实现自动移除通知,思路是在响应者 observerdealloc 的时候移除对应的通知,难点就是在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

变量的释放顺序各种不确定,可能走 YBObserverMonitordealloc 时, observer 响应者对象已经释放了,所以不直接使用 observer 响应者对象对比做释放操作。

写在后面

关于实现部分,虽然我做了个大致的测试,可能还是会存在一些潜在的问题,希望各位大佬不惜笔墨点拨一番

附: NSNotification 代码实现Demo地址

作者:indulge_in

链接:https://www.jianshu.com/p/e3a38b21420c


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First HTML and CSS

Head First HTML and CSS

Elisabeth Robson、Eric Freeman / O'Reilly Media / 2012-9-8 / USD 39.99

Tired of reading HTML books that only make sense after you're an expert? Then it's about time you picked up Head First HTML and really learned HTML. You want to learn HTML so you can finally create th......一起来看看 《Head First HTML and CSS》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换