iOS NSNotificationCenter 详解

4,032 阅读8分钟

这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

通知是Foundation 提供了一种编程架构,用于传递有关事件发生的信息。 本文档描述了该架构的元素并解释了如何使用它们。 接下来从三部分剖析通知相关知识 notifications,notification centers, notification queues.

Notifications

概述

Notification是封装了事件的信息,需要知道事件的对象(例如,需要知道其窗口何时关闭的文件)向通知中心注册,希望在该事件发生时得到通知。 当事件确实发生时,通知中心会发布通知,通知中心会立即将通知广播给所有已注册的对象。通知也可以在通知队列中排队,该队列在延迟指定通知后将通知发布到通知中心,并根据指定的某些指定条件合并相似的通知。

Notifications是什么

区别于标准的消息传递的形式(一个对象调用另一个对象的方法),通知使用了广播模型:一个对象发布一个通知,该通知通过一个 NSNotificationCenter 对象或简单的通知中心分派给适当的观察者.
一个 NSNotification 对象(称为通知)包含一个名称、一个对象和一个可选字典。 名称是标识通知的标签。 对象是通知的发布者想要发送给该通知的观察者的任何对象——通常是发布通知本身的对象。 字典可能包含有关事件的附加信息。

Notification 与代理

有以下区别:

  • 任何数量的对象都可以收到通知,而不仅仅是委托对象。即一对多。
  • 任一个对象都可以从通知中心接收消息,而不仅仅是预定义的委托方法。
  • 发布通知的对象甚至不必知道观察者的存在。

NotificationCenter

刚才介绍了Notification是什么,那NotificationCenter就是用来管理Notification发送和接收的。 通过Notification Centers来发送通知到所有观察者。 通知的具体信息封装在 NSNotification 对象中。大致过程就是当事件发生时,对象会向通知中心发布适当的通知,通知中心向每个注册的观察者发送一条消息,将通知作为唯一参数传递,当然发布对象和观察对象可能相同。

注:iOS中使用的NSNotificationCenter是单进程的通知中心类,区别于NSDistributedNotificationCenter(cocoa框架中的类,多进程管理的,暂不讨论)

通知中心同步向观察者发送通知。同步的原因很简单,NSNotificationCenter是单线程的,发送消息会根据添加观察者的顺序发送。也就是说当发布通知时,控制权不会返回到发布者,直到所有观察者都收到并处理了通知。 如果想要异步发送通知,就需要使用通知队列,Notification Queues中对此进行了描述。

Notification Queues

NSNotificationQueue通知队列,充当通知中心(NSNotificationCenter)的缓冲区。 NSNotificationQueue 类为 通知机制贡献了两个重要特性:通知的合并异步发布

Notification Queue 概述

使用 NSNotificationCenter 的 postNotification: 方法及其变体,可以将通知发布到通知中心。 但是,该方法的调用是同步的,也就是说再观察者接收到通知执行完对应的回调后才会往下执行。

如下示例:
ClassB中添加了通知监听

image.png

在vc中发送一个消息

image.png

打印结果如下:

image.png

notification send end是5s后打印的,也就是Classb中的回调结束后。

所以,在发布对象恢复其执行线程之前,它必须等到通知中心将通知分派给所有观察者并返回,并且每个线程都有一个默认的通知队列,它与进程的默认通知中心相关联。

当然也可以创建自己的通知队列,并且每个通知中心和线程有多个队列。且往下看。

Posting Notifications Asynchronously(异步发布)

使用 NSNotificationQueue 的 enqueueNotification:postingStyle:enqueueNotification:postingStyle:coalesceMask:forModes: 方法,可以通过将通知放入队列中来将通知异步发布到当前线程。 这些方法在将通知放入队列后立即返回调用对象。

注,如果在通知队列将通知发布到其通知中心之前,通知入队的线程终止,则不会发布通知。 上述方法中postingStyle、coalesceMask分别就对应了上文提到的比notificationcenter多的两个特性,发送时机与通知合并。

postingStyle表示发送通知的时机

NSPostWhenIdle

空闲时发送
仅当runloop处于等待状态(可以理解为休眠之前)时,才会发布此样式排队的通知。

比如,下面的示例即使在ClassB中添加了observe也是不会收到通知的,而如果line26换成一个属性变量就可以了。

image.png

注,即将退出的runloop未处于等待状态,因此不会发布通知。

NSPostASAP

尽快发送(As Soon As Possible)

image.png 官方说法有点抽象,可能是该时机比较复杂,简单理解就是事件结束后发送。

如下,当ViewDidLoad执行完成后,RunLoop中事件执行完成,立即发送(日志中,先打印notification send end,再执行B - didclick)。

image.png

NSPostNowPosting

立即发送,无需多言,且注意是合并后(如果有合并)。

image.png

coalesceMask表示合并策略

在某些情况下,如果给定事件至少发生一次,可能希望发布通知,但即使事件发生多次,也希望发布不超过一个通知。此时可以将通知添加到指定适当的合并选项的 NSNotificationQueue 实例。合并是一个从队列中删除通知的过程,这些通知在某种程度上类似于之前排队的通知。可以通过在 enqueueNotification:postingStyle:coalesceMask:forModes: 方法的第三个参数中指定以下一个或多个常量来指示相似性标准。

  • NSNotificationCoalescing 默认不合并
  • NSNotificationCoalescingOnName 同名合并
  • NSNotificationCoalescingOnsender 同object合并

Notification 注册与移除

调用通知中心方法 addObserver:selector:name:object: 注册一个对象以接收通知,指定观察者,通知中心应该发送给观察者的消息,它要接收的通知的名称,以及关于哪个对象。 一个观察者不再需要接收通知(例如,如果观察者正在被释放),使用方法 removeObserver:removeObserver:name:object: 从通知中心的观察者列表中删除该观察者。

说明:通知对应的是NSNotification对象,发送与接收通知其实是对该对象的操作,其发送方式无非是NSNotificationCenterNSNotificationQueue。再啰嗦两句这两种发送方式:
NSNotificationCenter,同步发送,我们可以理解为发送和接收消息需要一个过程(上文中已经使用延时模拟了),该过程结束之前后续的操作会同步进行,也就是最简单直接的发送与接收过程。
NSNotificationQueue,此种方式呢是对NSNotificationCenter功能的丰富,即发送时机与合并,合并不多做赘述,发送时机呢就是相较于NSNotificationCenter的同步,做了异步发送通知的过程及对postingStyle该枚举的取值。

线程与通知

默认情况下,消息的传递是在消息post的线程中进行的.有时,可能需要在由指定线程上传递通知,比如,如果在后台线程中运行的对象正在侦听来自用户界面的通知,例如窗口关闭,您希望在后台线程而不是主线程中接收通知.在这些情况下,必须捕获在默认线程上传递的通知并将它们重定向到适当的线程。

线程重定向的思路是使用自定义通知队列(不是 NSNotificationQueue 对象)来保存在错误线程上接收到的任何通知,然后在正确的线程上处理它们.该技术的工作原理如下,注册通知,当通知到达时,测试当前线程是否是应该处理通知的线程。如果是错误的线程,则将通知存储在队列中,然后向正确的线程发送信号,指示需要处理通知。另一个线程接收到信号,从队列中移除通知,并处理通知。

观察者需要有以下值的实例变量:

  • 用于保存通知的可变数组;
  • 用于向正确线程发出信号的通信端口(Mach 端口);
  • 用于防止多线程与通知数组冲突的锁;
  • 以及一个标识正确线程的值(一个 NSThread 对象);
#import <Foundation/Foundation.h>

@interface  MyThreadedClass : NSObject

/* Threaded notification support. */

@property NSMutableArray *notifications;

@property NSThread *notificationThread;

@property NSLock *notificationLock;

@property NSMachPort *notificationPort;

@end

实现相关

#import "MyThreadedClass.h"
@interface** MyThreadedClass ()<NSMachPortDelegate>
@end
@implementation** MyThreadedClass

-(instancetype)init{
     if (self = [super init]) {
        [self setUpThreadingSupport];
        [[NSNotificationCenter defaultCenter]addObserver:self selector: @selector(processNotification:) name:@"NotificationName" object:nil];

    }
      return self;
}
//初始化相关变量
- (void)setUpThreadingSupport {
      if (self.notifications) {
          return;
    }
      self.notifications      = [[NSMutableArray alloc] init];
      self.notificationLock   = [[NSLock alloc] init];
      //
      self.notificationThread = [NSThread currentThread];

      self.notificationPort = [[NSMachPort alloc] init];

    [self.notificationPort setDelegate:self];
//添加到runloop
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort
            forMode:NSRunLoopCommonModes];

}

//machport的回调
- (void)handleMachMessage:(void*)msg {

    [self.notificationLock lock];

    while ([self.notifications count]) {

        NSNotification *notification = [self.notifications objectAtIndex:0];
        [self.notifications removeObjectAtIndex:0];
        [self.notificationLock unlock];

        [self processNotification:notification];

        [self.notificationLock lock];

    };

    [self.notificationLock unlock];

}
//观察到通知的回调
- (void)processNotification:(NSNotification *)notification {

       //判断发送通知的线程是否是期望接收回调的线程,如果不是
      if ([NSThread currentThread] != _notificationThread) {

        // Forward the notification to the correct thread.
        [self.notificationLock lock];
        [self.notifications addObject:notification];
        [self.notificationLock unlock];
        [self.notificationPort sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];

    }

      else {
        //如果是 
        //dosomething
        // Process the notification here;
    }
}
@end