美文网首页iOS开发攻城狮的集散地原理篇程序员
透彻理解 NSNotificationCenter 通知(附实现

透彻理解 NSNotificationCenter 通知(附实现

作者: 波儿菜 | 来源:发表于2018-02-23 16:24 被阅读1242次

    博客更新日志:
    2018-04-12 —— 在 iOS9 及其之后版本不用在dealloc移除通知,而不是之前说的 iOS8

    推荐另一篇文章:透彻理解 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"}];
    //使用NSNotification
    NSNotification *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 : <NSThread: 0x60400046aec0>{number = 3, name = (null)}
    响应通知 currentThread : <NSThread: 0x60400046aec0>{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: 0x7f9a0a4d9240>
    

    神奇的现象发生了,通过比较内存地址,[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<NSObject>)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;
    }
    

    这里有个地方需要提出来谈谈,在使用系统的这个方法的时候,一经实验就能发现,不管我们强引用或者弱引用这个返回值id<NSObject>时,都能在业务类dealloc释放的时候有效的移除该通知。

    由于使用该方法添加通知的时候不会传入observer参数,这里创建了一个observer,如果这里使用observerInfo.observer = observer;,而业务类没有强引用这个返回值observer,它将会自然释放。所以,这里做了一个特殊处理,让observerInfo实例强持有observer

    值得注意的是,外部如果强引用返回的id<NSObject>类型的observer,会造成observer无法及时的释放,但是这点内存我认为还是可以接受的,当然业务类使用弱引用该observer是最好的选择。

    2、发送通知

    和系统通知一样,同样创建了一个类YBNotification发送通知消息体,属性就我们熟悉的几个:

    @property (copy) NSString *name;
    @property (weak) id object;
    @property (copy) NSDictionary *userInfo;
    

    然后将<NSCopying, NSCoding>两个协议实现一下就好了,具体看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.objectif语句中这个判断值得注意。

    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地址

    相关文章

      网友评论

      • 烟雨痕:通知是1对多的关系,因为可能有好几个控制器都需要监听通知,所以没去判断通知中心是否添加过该监听对象。在for循环添加通知确实不合适。
      • 烟雨痕: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
        烟雨痕:@indulge_in 我是在直接看苹果移除通知的API文档上看到的。你仔细看下。
        波儿菜:我在iOS8模拟器里面测试通知仍然会自动移除,没有在官方文档找到相关描述,可以告诉我在哪里看到的么?
        波儿菜:Excuse me,can you tell me where to find the description?
      • 未来行者:写的不错,但是"下面方法中,由于array.copy造成了局部变量,而上级可能是一个遍历,所以得加上自动释放池避免内存泄露。"这里应该不是避免内存泄露而是内存暴涨吧.另外和2楼一致,在大括号完了之后系统会自动释放范围内的内存(因为你是单独抽取的一个函数,而写在非在循环内),除非你手动添加autoreleasepool,所以显得有点没必要.
        波儿菜:@未来行者 哈哈是的 为此我写下了后一篇runloop文章:smile:
        未来行者:@indulge_in 确实,我验证了一下,当前任务是会被添加到最近创建的一个autoreleasepool里,然后等当前的runloop完成之后才会drain掉这个pool,手动添加是大括号完了之后就立马释放,这里涉及到了autoreleasepool作用域的问题.所以作者这里手动添加pool是对的.
        波儿菜:首先感谢提出,但是这里我得持保留意见。局部变量释放的判断标准是其对应线程的runloop走完一个周期,即依赖于当前runloop的自动释放池drain的时候才会统一释放。而当for循环内部有局部变量,若循环次数过大会导致内存堆积,因为当for循环结束对应的自动释放池才会drain。所以据我的思考这里可能还是加上@autoreleasepool比较保险,等有时间了我再去深入研究一下runloop,可能是我的理解有所偏差,现在暂且加上吧。谢谢😁
      • 6146bc11bfdc:写的好棒啊,其中对于array_removeObserverId:(NSString *)observerId object:(id)anObject array:(NSMutableArray *)array;方法中使用autoreleasepool有些疑问,使用autoreleasepool的目的应该是降低内存峰值吧,即使上层有遍历调用,但调用这个方法的话,array.copy这个局部变量随着方法调用的结束也会被回收的,所以应该不需要使用autoreleasepool吧
        波儿菜:感谢提出,回复写在了二楼。😁

      本文标题:透彻理解 NSNotificationCenter 通知(附实现

      本文链接:https://www.haomeiwen.com/subject/hysrtftx.html