美文网首页
[iOS] NSNotification和它的兄弟姐妹们

[iOS] NSNotification和它的兄弟姐妹们

作者: 木小易Ying | 来源:发表于2020-03-23 09:12 被阅读0次

    iOS我们最经常收到的就是push了,这也是通知的一种,但代码中我们更经常用到的大概是NSNotificationCenter。所以今天我就来学习一下NSNotification和他的兄弟NSNotificationCenter~

    1. NSNotificationCenter

    Cocoa中有2种通知中心,一种是NSNotificationCenter,它只能处理一个程序内部的通知,另一种是NSDistributedNotificationCenter(mas OS上才有),它能处理一台机器上不同程序之间的通知。

    NSNotificationCenter经常被我们用来app内传递消息,也就是设计模式里面的观察者模式,会有一个发消息的人以及一群收消息的人,这堆想接收消息的通过addObserver将自己注册给发消息的人,发送者只要调用postNotification就可以啦~

    先举个例子可以猜一下输出是啥~

    NSNotificationName const kNoti = @"kNoti";
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationReceived:) name:kNoti object:nil];
        
        [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:kNoti object:nil]];
        
        NSLog(@"viewDidLoad end");
    }
    
    - (void)notificationReceived:(NSNotification *)notification {
        NSLog(@"Noti VC notificationReceived %@", notification);
    }
    

    我最开始以为Notification和KVO是一样的,所以不会立刻回调addObserver里面的selector,但输出是酱紫的:

    2020-03-23 07:14:34.397690+0800 Example1[15738:3899887] Noti VC notificationReceived NSConcreteNotification 0x280802880 {name = kNoti}
    2020-03-23 07:14:34.397713+0800 Example1[15738:3899887] viewDidLoad end
    

    也就是说当我们调用postNotification发送消息的时候,会block在这句话,直到所有observer的selector被调用结束~

    有木有觉得这个有点像代理?delegate也是酱紫的,但是notification好像就是有了一群代理,他俩的区别是啥嘞?
    1. delegate是一对一的,notification observer是一对多的
    2. delegate可以有返回值,注册notification observer是木有的,只是单纯被动的被触发了一个selector
    3. delegate是不通用的,如果你想增加一个响应者,可能需要重新定义一组protocol然后加一个新的delegate,发送者是需要知道有多少delegate也就是响应者然后为他们量身定制protocol;但notification observer的形式只需要增加一个observer即可,或者有可能再加个notification name,发送者本身是不需要知道有木有observer这个事儿的,它增加的代码比较通用

    遇到notification center我会有两个比较懵的问题,一个是继承,另一个是线程,所以后面都来看一下哈~

    (1)observer的继承

    如果A将自己addObserver了,B继承A然后在init的时候也addObserver,那么当我们初始化B以后,发通知的时候究竟会调用A还是B的selector呢?

    // NotiA
    NSNotificationName const kNoti = @"kNoti";
    
    @implementation NotiA
    
    - (instancetype)init {
        if (self = [super init]) {
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationReceived:) name:kNoti object:nil];
        }
        
        return self;
    }
    
    - (void)notificationReceived:(NSNotification *)notification {
        NSLog(@"NotiA notificationReceived %@", notification);
    }
    
    @end
    
    ===================
    
    // NotiB
    extern NSNotificationName const kNoti;
    
    @implementation NotiB
    
    - (instancetype)init {
        if (self = [super init]) {
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationReceived:) name:kNoti object:nil];
        }
        
        return self;
    }
    
    - (void)notificationReceived:(NSNotification *)notification {
        NSLog(@"NotiB notificationReceived %@", notification);
    }
    
    @end
    
    ===================
    
    // VC
    @implementation NotiViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        NotiB *b = [[NotiB alloc] init];
        [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:kNoti object:nil]];
        NSLog(@"viewDidLoad end");
    }
    
    @end
    

    然后输出是酱紫的:

    2020-03-23 07:42:13.303911+0800 Example1[15809:3910466] NotiB notificationReceived NSConcreteNotification 0x283c04120 {name = kNoti}
    2020-03-23 07:42:14.105997+0800 Example1[15809:3910466] NotiB notificationReceived NSConcreteNotification 0x283c04120 {name = kNoti}
    2020-03-23 07:42:14.106405+0800 Example1[15809:3910466] viewDidLoad end
    

    也就是说实例B其实注册了两次observer,所以回调了两次B的notificationReceived。如果我们把NotiA中的selector名字改掉,就能回调一次A再回调一次B啦~

    注意如果注册的selector并不存在,会crash的哦~

    1. 所以addObserver这个事儿没有去重,add几次就回调几次
    2. 如果继承关系的两个类都要监听同一个noti,selector不要名字一样,否则只会调用子类的方法

    (2)post notification的线程

    日常我们有时需要在观察者的selector中将UI操作抛到主线程,就是为了防止发消息的不是在主线程,此时回调也不会在主线程,我们来坐下实验。

    其实这个也很好理解,毕竟postNotification是同步调用它的observer的方法的,自然被调用的方法和postNotification是同一线程的~~

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NotiB *b = [[NotiB alloc] init];
            NSLog(@"Noti VC notification Send thread %@", [NSThread currentThread]);
            
            [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:kNoti object:nil]];
        });
    }
    
    输出:
    2020-03-23 07:58:06.178450+0800 Example1[15837:3917243] Noti VC notification Send thread <NSThread: 0x2837f7000>{number = 3, name = (null)}
    2020-03-23 07:58:06.178652+0800 Example1[15837:3917243] NotiB notificationReceived thread <NSThread: 0x2837f7000>{number = 3, name = (null)}
    

    可以看到消息发送的线程就是回调所在线程,这就要求我们在注册监听的时候要注意selector所在的线程有可能不是主线程哦~

    但其实addObserverForName:object:queue:usingBlock:可以指定回调的队列,常用于指定主队列~

    [[NSNotificationCenter defaultCenter] addObserverForName:kNoti object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"NotiB notificationReceived thread %@", [NSThread currentThread]);
    }];
    

    (3)object

    可以参考:https://www.jianshu.com/p/da4fa2f797d2

    notification在创建的时候以及我们addObserver的时候都有一个object参数,这个参数是干啥的呢?

    其实object参数是用于帮我们确定我们要监听谁发出来的通知,因为任何人都可以post notification,但是我们可能只要某个人的通知~

    发送者 观察者 是否可以收到通知
    post填nil add填XXX
    post填XXX add填nil
    post填XXX add填XXX
    post填nil add填nil

    官方解释最后一个参数为notificationSender,解释如下:
    The object whose notifications the observer wants to receive; that is, only notifications sent by this sender are delivered to the observer. When nil, the notification center doesn’t use a notification’s sender to decide whether to deliver it to the observer.

    官方并没有对object参数做限制,但是正确使用NSNotification的姿势应该是所有想进行传递的参数都应该放在userInfo中,object只作为收发通知的一个限制要求。


    2. NSNotificationQueue(通知队列)

    在最开始的时候我们验证了postNotification是同步的,会等所有selector被调用结束以后才会继续,那么如果我们想让他异步发消息要怎么办呢?

    notification queue

    每个线程都有一个缺省的通知队列[NSNotificationQueue defaultQueue],注意notificationCenter是每个进程有一个,也就是app自己公用一个。

    NSNotificationQueue与NotificationCenter相关联,开发者可以自己创建通知队列,并且每个通知中心和线程都可以有多个队列。

    向队列投递一个异步通知方法如下:

    - (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
    
    - (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSUInteger)coalesceMask forModes:(NSArray *)modes;
    

    这俩方法有三个参数分别是:

    • postingStyle:啥时候发
    • coalesceMask:要不要合并发送,按照什么合并
    • modes:要不要规定runLoop在哪些mode的时候才发送
    (1)postingStyle
    typedef NS_ENUM(NSUInteger, NSPostingStyle) {
        NSPostWhenIdle = 1,
        NSPostASAP = 2,
        NSPostNow = 3
    };
    

    举个例子:

    NotiB *b = [[NotiB alloc] init];
    [[NSNotificationQueue defaultQueue] enqueueNotification:[NSNotification notificationWithName:kNoti object:nil] postingStyle:NSPostWhenIdle];
    

    改成这样以后会发现NotiB的notificationReceived方法没有被触发,因为noti不会立刻被发出,它发出的时候对象b已经被销毁了(没有强引用),所以并不会触发回调。

    那么这三种style的区别是啥呢?

    • => NSPostASAP (尽快发送 Posting As Soon As Possible)

    以NSPostASAP风格进入队列的通知会在运行循环RunLoop的当前迭代完成时被发送给通知中心,如果当前运行循环模式RunLoopMode和请求的模式相匹配的话(如果请求的模式和当前模式不同,则通知在进入请求的模式时被发出)。

    由于运行循环在每个迭代过程中可能进行多个调用分支(callout),所以在当前调用分支退出及控制权返回运行循环时,通知可能被分发,也可能不被分发。其它的调用分支可能先发生,比如定时器或由其它源触发了事件,或者其它异步的通知被分发了。

    开发者通常可以将NSPostASAP风格用于开销昂贵的资源,比如显示服务器。如果在运行循环的一个调用分支过程中有很多客户代码在窗口缓冲区中进行描画,在每次描画之后将缓冲区的内容刷新到显示服务器的开销是很昂贵的。在这种情况下,每个draw...方法都会将诸如“FlushTheServer” 这样的通知排入队列,并指定按名称和对象进行合并,以及使用NSPostASAP风格。结果,在运行循环的最后,那些通知中只有一个被派发,而窗口缓冲区也只被刷新一次

    • => NSPostWhenIdle(空闲时发送)

    以NSPostWhenIdle风格进入队列的通知只在运行循环处于等待状态时才被发出。在这种状态下,运行循环的输入通道中没有任何事件,包括定时器和异步事件。

    以NSPostWhenIdle风格进入队列的一个典型的例子是当用户键入文本、而程序的其它地方需要显示文本字节长度的时候。在用户输入每一个字符后都对文本输入框的尺寸进行更新的开销是很大的(而且不是特别有用),特别是当用户快速输入的时候。在这种情况下,Cocoa会在每个字符键入之后,将诸如“ChangeTheDisplayedSize”这样的通知进行排队,同时把合并开关打开,并使用NSPostWhenIdle风格。当用户停止输入的时候,队列中只有一个“ChangeTheDisplayedSize”通知(由于合并的原因)会在运行循环进入等待状态时被发出,显示部分也因此被刷新。请注意,运行循环即将退出(当所有的输入通道都过时的时候,会发生这种情况)时并不处于等待状态,因此也不会发出通知。

    • => NSPostNow(立即发送)

    以NSPostNow风格进入队列的通知会在合并之后,立即发送到通知中心。开发者可以在不需要异步调用行为的时候 使用NSPostNow风格(或者通过NSNotificationCenter的postNotification:方法来发送)。在很多编程环境下,我们不仅允许同步的行为,而且希望使用这种行为:即开发者希望通知中心在通知派发之后返回,以便确定观察者对象收到通知并进行了处理。

    当然,当开发者希望通过合并移除队列中类似的通知时,应该用enqueueNotification...方法,且使用NSPostNow风格,而不是使用postNotification:方法。


    我们尝试给RunLoop加个observer看一下~

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
                switch (activity) {
                    case kCFRunLoopEntry:
                        NSLog(@"RunLoop进入");
                        break;
                    case kCFRunLoopBeforeTimers:
                        NSLog(@"RunLoop要处理Timers了");
                        break;
                    case kCFRunLoopBeforeSources:
                        NSLog(@"RunLoop要处理Sources了");
                        break;
                    case kCFRunLoopBeforeWaiting:
                        NSLog(@"RunLoop要休息了");
                        break;
                    case kCFRunLoopAfterWaiting:
                        NSLog(@"RunLoop醒来了");
                        break;
                    case kCFRunLoopExit:
                        NSLog(@"RunLoop退出了");
                        break;
                        
                    default:
                        break;
                }
            });
            
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        
        self.b = [[NotiB alloc] init];
        
        [[NSNotificationQueue defaultQueue] enqueueNotification:[NSNotification notificationWithName:kNoti object:nil] postingStyle:NSPostWhenIdle];
        
        NSLog(@"viewDidLoad end");
    }
    

    输出是酱紫:

    2020-03-23 10:22:56.351147+0800 Example1[15984:3965869] viewDidLoad end
    2020-03-23 10:22:56.397724+0800 Example1[15984:3965869] RunLoop要处理Timers了
    2020-03-23 10:22:56.397781+0800 Example1[15984:3965869] RunLoop要处理Sources了
    2020-03-23 10:22:56.401687+0800 Example1[15984:3965869] RunLoop要处理Timers了
    2020-03-23 10:22:56.401747+0800 Example1[15984:3965869] RunLoop要处理Sources了
    2020-03-23 10:22:56.408809+0800 Example1[15984:3965869] RunLoop要处理Timers了
    2020-03-23 10:22:56.408874+0800 Example1[15984:3965869] RunLoop要处理Sources了
    2020-03-23 10:22:56.409019+0800 Example1[15984:3965869] RunLoop要处理Timers了
    2020-03-23 10:22:56.409049+0800 Example1[15984:3965869] RunLoop要处理Sources了
    2020-03-23 10:22:56.409077+0800 Example1[15984:3965869] RunLoop要处理Timers了
    2020-03-23 10:22:56.409099+0800 Example1[15984:3965869] RunLoop要处理Sources了
    2020-03-23 10:22:56.409120+0800 Example1[15984:3965869] RunLoop要休息了
    2020-03-23 10:22:56.409259+0800 Example1[15984:3965869] NotiA notificationReceived NSConcreteNotification 0x281444180 {name = kNoti}
    2020-03-23 10:22:56.410487+0800 Example1[15984:3965869] RunLoop醒来了
    2020-03-23 10:22:56.410520+0800 Example1[15984:3965869] RunLoop要处理Timers了
    2020-03-23 10:22:56.410547+0800 Example1[15984:3965869] RunLoop要处理Sources了
    2020-03-23 10:22:56.410568+0800 Example1[15984:3965869] RunLoop要休息了
    2020-03-23 10:22:56.413143+0800 Example1[15984:3965869] RunLoop醒来了
    2020-03-23 10:22:56.413201+0800 Example1[15984:3965869] RunLoop要处理Timers了
    2020-03-23 10:22:56.413223+0800 Example1[15984:3965869] RunLoop要处理Sources了
    2020-03-23 10:22:56.413243+0800 Example1[15984:3965869] RunLoop要休息了
    

    然后换成ASAP的方式输出是酱紫的~

    2020-03-23 10:26:41.402757+0800 Example1[15989:3967374] viewDidLoad end
    2020-03-23 10:26:41.443787+0800 Example1[15989:3967374] RunLoop要处理Timers了
    2020-03-23 10:26:41.443961+0800 Example1[15989:3967374] NotiA notificationReceived NSConcreteNotification 0x282ee8630 {name = kNoti}
    2020-03-23 10:26:41.444004+0800 Example1[15989:3967374] RunLoop要处理Sources了
    2020-03-23 10:26:41.447681+0800 Example1[15989:3967374] RunLoop要处理Timers了
    2020-03-23 10:26:41.447724+0800 Example1[15989:3967374] RunLoop要处理Sources了
    2020-03-23 10:26:41.453708+0800 Example1[15989:3967374] RunLoop要处理Timers了
    2020-03-23 10:26:41.453752+0800 Example1[15989:3967374] RunLoop要处理Sources了
    2020-03-23 10:26:41.453897+0800 Example1[15989:3967374] RunLoop要处理Timers了
    2020-03-23 10:26:41.453925+0800 Example1[15989:3967374] RunLoop要处理Sources了
    2020-03-23 10:26:41.453953+0800 Example1[15989:3967374] RunLoop要处理Timers了
    2020-03-23 10:26:41.453973+0800 Example1[15989:3967374] RunLoop要处理Sources了
    2020-03-23 10:26:41.453997+0800 Example1[15989:3967374] RunLoop要休息了
    2020-03-23 10:26:41.458068+0800 Example1[15989:3967374] RunLoop醒来了
    

    明显比idle要快很多,它是在当前的这个事儿干完就会做了,不会等runloop到waiting状态。

    如果改成Now的方式输出是酱紫的,其实和同步类似~(区别只是之后我们即将看的合并):

    2020-03-23 10:28:43.025103+0800 Example1[15993:3968351] NotiA notificationReceived NSConcreteNotification 0x283c5c780 {name = kNoti}
    2020-03-23 10:28:43.025128+0800 Example1[15993:3968351] viewDidLoad end
    2020-03-23 10:28:43.071333+0800 Example1[15993:3968351] RunLoop要处理Timers了
    

    What is the difference between enqueuing notifications with NSPostNow and posting notifications (using NSNotificationCenter's postNotification... methods)? Both post notifications immediately (but synchronously) to the notification center. The difference is that enqueueNotification:... (with NSPostNow as the posting style) coalesces notifications in the queue before posting while postNotification: does not.


    可以看到NSNotificationQueue的定义是酱紫的:

    @interface NSNotificationQueue : NSObject {
    @private
        id      _notificationCenter;
        id      _asapQueue;
        id      _asapObs;
        id      _idleQueue;
        id      _idleObs;
    }
    

    也就是我们的NSNotificationQueue里面不只一个队列,对于idle和asap模式是有不同队列管理的。而且这俩严重依赖了runloop,可能会有没有被触发的状况(例如runLoop退出)。

    (2)coalesceMask

    这个参数是处理消息合并的,也就是可以在post多个noti的时候变成只post一个。其实这个就和dispatch里面有个add模式是类似的,但区别是那个是累计例如下载的时候会把一段时间下载了多少加和给到callback,但是消息合并是只留一个不会加和哦。

    typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
        NSNotificationNoCoalescing = 0, // 不合并
        NSNotificationCoalescingOnName = 1, // 根据noti名字合并
        NSNotificationCoalescingOnSender = 2 // 根据noti发送者合并
    };
    

    虽然说postNow的方式enqueue,和postNotification的区别就是collapse notification,但是讲真我并没有尝试出在NSPostNow的方式下还merge,如果有朋友知道怎么搞出来欢迎私我~

    所以这里还是用另外的来测试合并了~

    for (NSInteger i = 0; i < 10; i++) {
        NSNotification *noti = [NSNotification notificationWithName:kNoti object:nil];
        NSLog(@"enqueue noti: %@", noti);
        [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:nil];
    }
    
    输出:
    2020-03-23 11:45:03.851970+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x2810233f0 {name = kNoti}
    2020-03-23 11:45:03.852030+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x281025e00 {name = kNoti}
    2020-03-23 11:45:03.852063+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x28102d830 {name = kNoti}
    2020-03-23 11:45:03.852090+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x281020780 {name = kNoti}
    2020-03-23 11:45:03.852116+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x28102d230 {name = kNoti}
    2020-03-23 11:45:03.852143+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x281020780 {name = kNoti}
    2020-03-23 11:45:03.852170+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x28102d830 {name = kNoti}
    2020-03-23 11:45:03.852196+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x281020780 {name = kNoti}
    2020-03-23 11:45:03.852218+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x28102d230 {name = kNoti}
    2020-03-23 11:45:03.852613+0800 Example1[16099:3999852] enqueue noti: NSConcreteNotification 0x2810333f0 {name = kNoti}
    2020-03-23 11:45:03.852636+0800 Example1[16099:3999852] viewDidLoad end
    2020-03-23 11:45:03.896570+0800 Example1[16099:3999852] NotiB notificationReceived NSConcreteNotification 0x2810233f0 {name = kNoti}
    

    首先,这里可以看到notification对象可能是重用的,在noti名字一样的时候会发现创建的对象是一个;另外,虽然我们enqueue了十次,但是实际上回调就1次,当可以发的时候会把第一个之后的都取消掉(这个是我试了几次好像是这样),然后只保留一个给notification center。


    3. NSNotification

    NSNotification比较简单,只有name、object以及userInfo,需要注意的是,NSNotification是一个不可变的对象:

    @interface NSNotification (NSNotificationCreation)
    
    + (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
    + (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
    
    - (instancetype)init /*API_UNAVAILABLE(macos, ios, watchos, tvos)*/;    /* do not invoke; not a valid initializer for this class */
    
    @end
    

    之前我们已经提到过object主要是用于区分sender的,所以如果sender想传递一些参数给监听者,只能用userInfo啦

    字段名 含义
    name 通知的名称,用于通知的唯一标识
    object 保存发送通知的对象
    userinfo 保存给通知接受者传递的额外信息

    可以使用notificationWithName:object:或者notificationWithName:object:userInfo:创建通知对象,但是一般情况下不会这样直接创建。实际工作中更多是直接使用NSNotificationCenter调用postNotificationName:object:或者 postNotificationName:object:userInfo:方法发出通知,这两个方法会在内部直接创建这个对象。

    NSNotification是一个类簇,不能通过init实例化,比如NSNotification *notif = [[NSNotification alloc]init];这样会引起crash。

    如果想要附加更多信息在NSNotification中,可以子类化NSNotification,额外新加的字段。需要注意的一点就是虽然可以自己去实现装饰构造方法,但是切记在自定义的装饰构造方法中不要调用[super init]。


    4. 原理

    在NSNotificationCenter内部一共保存了两张表Named Table & UnNamed Table。一张用于保存添加观察者的时候传入了NotifcationName的情况;一张用于保存添加观察者的时候没有传入了NotifcationName的情况,下面分两种情况分析。

    typedef struct NCTbl {
      Observation   *wildcard;  /* 保存既没有没有传入通知名字也没有传入object的通知*/
      MapTable       nameless;   /*保存没有传入通知名字的通知 */
      MapTable       named; /*保存传入了通知名字的通知 */
    } NCTable;
    

    话说这里我才知道其实可以不传name添加监听的~ 这种情况就是任何notification都可以监听到~

    ※ Named Table
    Named Table

    在Named Table中,NotifcationName作为表的key,因为我们在注册观察者的时候是可以传入一个参数object用于只监听指定该对象发出的通知,并且一个通知可以添加多个观察者,所以还需要一张表来保存object和Observer的对应关系。这张表的是key、Value分别是以object为Key,Observer为value。如何来实现保存多个观察者的情况呢?用链表这种数据结构最合适不过了。

    所以对于Named Table而已,最终的结构:

    • 首先外层有一个Table,以通知名称为Key。其Value同样是一个Table(简称内Table).
    • 为了实现可以传入一个参数object用于只监听指定该对象发出的通知,及一个通知可以添加多个观察者。则内Table的以传入的Object为Key,用链表来保存所有的观察者,并且以这个链表为Value。

    在实际开发中我们经常传一个nil的object。这个时候系统会根据nil自动生产一个key(可以理解为一个nil_key)。相当于这个key对应的value(链表)保存的就是对于当前NotifcationName没有传入object的所有观察者。当NotifcationName被发送时,所以在链表中的观察者都会收到通知。

    ※ UnNamed Table

    UnNamed Table结构比Named Table简单很多。因为没有NotifcationName作为Key。这里直接就以object为key。比Named Table少了一层Table嵌套。

    UnNamed Table

    如果在注册观察者的时候既没有NotifcationName,同时没有传入Object。经过代码实践,所以的系统通知都会发送到注册的对象这里。恰恰对应到上面提到的数据结构中的wildcard字段。


    (1)addObserver
    • 首先会根据传入的参数,实例化一个Observation。这个Observation保存了观察者对象、接收到通知观察者对所执行的方法,由于Observation是一个链表,还保存了下一个Observation的地址。

    • 根据是否传入通知的Name选择在Named Table还是UNamed Table操作。

    • 如果传入通知的Name,则会先去用Name去查找是否已经有对应的Value(注意这个时候返回的Value是一个Table)
      如果没有对应的Value,则创建一个新的Table,然后将这个Table以Name为Key添加到Named Table。如果有Value,那么直接去取出这个Table。

    • 得到了保存Observation的Table之后,就通过传入的object去拿对应的链表。如果object为空,会默认有一个key表示传入object为空的情况,取的时候也会直接用这个key去取。表示所有任何地方发送通知都会监听。

    • 如果在保存Observation的Table中根据object作为key没有找到对应的链表,则会创建一个节点,作为头结点插入进去;如果找到了则直接在链表末尾插入之前实例化好的Observation。

    在没有传入通知名字的情况和上面的过程类似,只不过是直接根据object去对应的链表而已。如果既没有传入NotifcationName也没有传入Object。则这个观察者会添加到wildcard(在介绍Table数据结构中提到够)链表中。


    (2)postNotification

    发送通知的一般是调用postNotificationName:(NSNotificationName)aName object:(nullable id)anObject来实现。

    postNotificationName内部会实例化一个NSNotification来保存传入的各种参数。根据之前介绍的数据结构,包含name、object和一个userinfo。

    发送通知的流程总体来讲就是根据NotifcationName查找到对应的Observer链表,然后遍历整个链表,给每个Observer结点中保持存的对象及SEL,来像对象发送信息(也即是调用对象的SEL方法)

    • 首先会定义一个数组ObserversArray来保存需要通知的Observer。之前在添加观察者的时候把既没有传入NotifcationName也没有传入object保存在了wildcard。因为这样的观察者会监听所有NotifcationName的通知,所以先把wildcard链表遍历一遍,将其中的Observer加到数组中ObserversArray

    • 找到以object为key的Observer链表。这个过程分为在Named Table中找,以及在UNamed Table中查找。然后将遍历找到的链表,同样加入到最开始创建的数组ObserversArray中。

    • 至此所有关于NotifcationName的Observer(wildcard+UNamed Table+Named Table)已经加入到了数组ObserversArray。接下来就是遍历这个ObserversArray数组,一次取出区中的Observer结点。因为这个几点保存了观察者对象以及selector。所以最终调用形式如下:
      [observerNode->observer performSelector: o->selector withObject: notification];


    (3)removeObserver

    根据前面分析的添加观察及发送通知的流程可以类比出移除通知的流程是如何的。掌握好核心就是操作两个Table及一个链表(wildcard)。

    参考:
    https://www.jianshu.com/p/209ef870e131
    原理部分完全参考大神的:https://www.jianshu.com/p/83770200d476

    相关文章

      网友评论

          本文标题:[iOS] NSNotification和它的兄弟姐妹们

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