阅读 405

Notification Programming Topics

Notification Programming Topics

Notifications

  • 通知封装了事件的信息, 例如窗口获得焦点或网络连接关闭
  • 需要关注事件状态的观察者会被通知中心通知, 通知中心会进行广播, 向所有的观察者发出通知
  • 可选择的将通知添加到通知队列中, 比如延迟指定某些通知, 合并某些指定条件相似的通知,它将通知发布到通知中心

Notifications and Their Rationale

  • 在两个对象间传递信息标准的方式是发送消息, 一个对象调用另一个对象的方法, 但是消息传递要求发送消息的对象知道接收方是谁以及它响应什么消息, 与此同时这种两个对象的耦合是不可取的, 所以使用NSNotificationCenter通知中心进行消息广播是比较好的方式
  • 一个NSNotification对象包含一个名称name, 一个对象object, 和一个可选的字典dictionary, 名称表示了是哪个通知, 对象是发送者想要发送给观察者的任意对象(通常把发送者自己发过去, 注意发送者内存释放问题), 字典通常存储关于事件的额外信息

注意

  • 关于通知的名称, 在代码中最好不要直接出现字符串常, 而是使用类似NSWindowDidBecomeMainNotification这样的常量名去代替
  • 对于观察者至少要知道通知的名称, 如果通知有提供字典, 还要明确字典中的key

Notification and Delegation

通知和代理的差异

  • 可向对个对象发送通知, 而不仅仅是代理对象, 所以也就不存在返回值这么一说
  • 一个对象可以接收来自消息中心的任何对象, 不只是来自于预定义的委托方法的消息
  • 对象发送通知不需要关心是否有观察者

Notification Centers

Cocoa包含两种通知中心

The NSNotificationCenter class manages notifications within a single process.

The NSDistributedNotificationCenter class manages notifications across multiple processes on a single computer.

  • NSNotificationCenter 通过单一进程管理通知对象
  • NSDistributedNotificationCenter 在一台计算机上使用多进程管理通知对象

NSNotificationCenter

  • 每一个进程(运行的应用)都有默认的通知中心, 通过NSNotificationCenterdefaultCenter方法获取, 在同一台机子上要多进程通知, 就得使用NSDistributedNotificationCenter
  • 通知中心是以同步的方式给观察者发送通知消息, 当发送通知后, 只有所有的接受者收到消息了, 才会返回到发送者, 如果要异步发送通知, 则要使用Notification Queues
  • 在多线程应用中, 通知始终在发布通知的线程中传递,该线程可能不是观察者注册的那个线程

NSDistributedNotificationCenter

  • 每一个进程都有自己默认的分布式通知中心, 通过NSDistributedNotificationCenterdefaultCenter方法获取, 能够处理一台机子上不同进程间的通知, 使用distributed objects(查看Distributed Objects Programming Topics)进行进程间的通讯
  • 发送分布式通知是一个开销比较大的操作, 通知会发送到系统范围能的观察者, 发布通知和通知到达另一个进程之间的延迟是无界的, 如果发布的通知太多,并且服务的队列已满,则会删除通知
  • 分布式通知是通过进程的run loop进行传递的, 进程必须运行某个common模式的run loop, 例如NSDefaultRunLoopMode, 去接收分布式通知的, 如果进程是多线程的, 不要依赖到达主线程的通知, 通知通常由主线程的run loop传递, 但是其他线程也能够接收到
  • 通常通知中心可以观察任何对象, 分布式通知中心仅限于观察字符串对象, 由于发布对象和观察者可能位于不同的进程中,因此通知不能包含指向任意对象的指针, 通知匹配是基于此字符串而不是对象指针完成的

Notification Queues

NSNotificationQueue是作为通知中心NSNotificationCenter实例的buffers, 包含了 Foundation Kit通知的两个重要特性通知的合并, 异步发布

Notification Queue Basics

  • 通知队列先进先出方式管理通知对象, 当一个通知对象出队时, 队列会将通知添加到通知中心, 然后派发通知给所有注册的观察者
  • 每个线程都有默认的通知队列, 与进程的默认通知中心相关, 可创建自己的通知队列, 每个通知中心和线程都可以有多个通知队列

Posting Notifications Asynchronously

  • 在当前线程将通知对象添加到通知队列中, 可执行异步发送

    • 使用NSNotificationQueue方法enqueueNotification:postingStyle:enqueueNotification:postingStyle:coalesceMask:forModes:进行操作
    • 将通知对象添加到队列后就会立即返回
    • 如果在队列将通知添加到通知中心前, 所在线程终止了, 则通知对象不会被发布出去
  • 通知队列清空操作和发送通知是基于发送类型(posting style)和特定的run loop模式(Mode), mode参数指定特定的运行循环模式下将清空队列, 例如指定了NSModalPanelRunLoopMode模式, 通知就会在run loop处于这种模式下才会被发送, 如果run loop当前不是这种模式, 则需要等待切换至此模式时发挥发出通知

通知队列的类型

  • Posting As Soon As Possible NSPostASAP
    • 当前run loop当前迭代结束(一个周期)将通知对象发送到通知中心, 当然当前run loop处于在给定的run loop mode下, 如果不在指定的mode下, 通知会在切换到此mode的时候发送到通知中心, 因为在run loop一个周期循环中, 会有多个callout(唤出执行?), 通知不一定会立马被callout, 其他的callout可能会先执行, 例如timer, source执行或是其他异步通知被传递
    • NSPostASAP一般用于比较昂贵的源, 例如画面显示服务(和界面UI相关), 见面显示所以要尽快的刷新绘制buffer, 通常就会发送FlushTheServer这样的通知, 这些通知中只有一个会派发到run loop,并且只绘制刷新一次
  • Posting When Idle NSPostWhenIdle
    • 只有run loop处于wait状态时, 通知才会被发送, 在wait状态下, run loop的输入通道内, 没有其他任何东西(timer, 异步事件等) NSPostWhenIdle类型举例: 用户输入文本字符, 程序显示文本大小, 当用户输入很快的时候, 边打字变现实文本大小是一件开销很大的事情,而且还不实用, 此时就可以让程序发送一个ChangeTheDisplayedSize这样的通知, 使用NSPostWhenIdle这个类型去发送的, 当用户停止输入的时候, run loop处于wait状态, 接收到通知然后去更新文本大小, 注意run loop退出的情况, 退出了就收不到消息了
  • Posting Immediately NSPostNow
    • 通过NSPostNow类型方式入队的通知对象会在合并到通知中心的时候立即发送
    • 不需要异步发送通知的时候使用NSPostNow(或者使用postNotification:), 同步操作也是需要的, 有时候在发出通知后需要等待所有的接受者就收到消息再返回
    • 当在要合并并删除一些类似的通知的时候, 应该使用enqueueNotification...NSPostNow的方式, 而不是使用postNotification:

Coalescing Notifications

会有这样的需求, 事件产生多次就只发送一次通知(通常是事件产生一次就发送一次通知), 例如在以离散数据包形式接收数据的应用程序中,希望在收到数据包后发布通知以表明需要处理数据, 在给定时间内到达了多个数据包, 但是并不想发多个通知, 此外, 发布通知的对象并不知道到底有多少个数据包到达, 发送通知方法是都在一个循环迭代中

在某些情况下,可以简单地设置一个布尔标志(无论是对象的实例变量还是全局变量)以表示事件已发生,并禁止发布进一步的通知,直到该标志被清除为止, 使用NSNotificationCenter这种同步发送通知的方式是无法做到忽略某些重复通知的, 通知中心也无法得知是否会有多个通知到达

因此, 可以将通知对象添加到NSNotificationQueue对象中, 队列可以在合适的时机进行通知的合并, 合并过程是从队列中删除在某种程度上类似于先前已排队的通知

在方法enqueueNotification:postingStyle:coalesceMask:forModes:的如下参数用以指定通知的一个或多个相似性

  • NSNotificationNoCoalescing
  • NSNotificationCoalescingOnName
  • NSNotificationCoalescingOnSender

可以还用按位或NSNotificationCoalescingOnNameNSNotificationCoalescingOnSender方式来指定通知的名称和对象来合并


// MyNotificationName defined globally
NSString *MyNotificationName = @"MyNotification";
 
id object = <#The object associated with the notification#>;
NSNotification *myNotification =
        [NSNotification notificationWithName:MyNotificationName object:object]
[[NSNotificationQueue defaultQueue]
        enqueueNotification:myNotification
               postingStyle:NSPostWhenIdle
               coalesceMask:NSNotificationCoalescingOnName
                   forModes:nil];
复制代码

Registering for a Notification

Registering for Local Notifications


[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(aWindowBecameMain:)
                                             name:NSWindowDidBecomeMainNotification 
                                           object:nil];

- (void)aWindowBecameMain:(NSNotification *)notification {
 
    NSWindow *theWindow = [notification object];
    MyDocument = (MyDocument *)[[theWindow windowController] document];
 
    // Retrieve information about the document and update the panel.
}
复制代码

Registering for Distributed Notifications

分布式通知的对象通过NSDistributedNotificationCenter对象的方法addObserver:selector:name:object:suspensionBehavior:来注册观察者, 指定发送的消息, 名称, 匹配的标识字符串(object参数), 以及通知传递被暂停时的行为

由于分布式通知是应用于多个进程间通讯, 通知对象不能保留指针类型的变量, 指针指向的对象, 所以使用NSString对象来做object参数值, 所以要定义好字符串表示意义, 好好查看文档

当一个进程不需要在接收通知了, 则要暂停发送通知对象, 这个操作通常在隐藏应用程序或将其置于后台时执行(当应用不活跃的时候NSApplication对象就会暂停发送)

再添加观察者方法里的suspendBehavior参数标识当通知传递被暂停的时候, 应该如何处理通知对象

  • NSNotificationSuspensionBehaviorDrop

    • 收到的通知对象/名称不会入队
    • 当收到setSuspended:NO(挂起结束了)才会正常执行
  • NSNotificationSuspensionBehaviorCoalesce

    • 相同的通知对象/名称只会讲最有一个进行入队, 其他的都会被抛弃
    • NSNotificationSuspensionBehaviorCoalesce是默认参数
  • NSNotificationSuspensionBehaviorHold

    • server会保留所有匹配的通知,直到队列已满(server确定的队列大小)为止,此时server可以刷新已排队的通知
  • NSNotificationSuspensionBehaviorDeliverImmediately

    • 无论是否收到setSuspended:YES消息,server都会传递与该注册匹配的通知
    • 匹配具有此暂停行为的通知后,其效果是首先刷新所有排队的通知
    • server在应用程序挂起时收到setSuspended:NO消息,随后传递有问题的通知,然后过渡到先前的挂起或未挂起状态

通过setSuspended:YES可以暂停分布式通知中心发送通知, 程序恢复通知传递后, 所有的通知队里会立即执行通知传递, 使用了Application Kit的应用中, NSApplication对象通常会在应用不活跃的时候暂停通知传递

注意使用NSNotificationSuspensionBehaviorDeliverImmediately方式注册观察者观察的通知对象, 当其传递的时候会刷新通知队列, 导致排队中的通知也都会进行发送

通知的发布者可以覆盖挂起状态, 如果一个通知很紧急, 比如通知一个server宕机的警告, 发送者可以强制通知立即发送给所有的观察者, 在NSDistributedNotificationCenter对象的方法postNotificationName:object:userInfo:deliverImmediately:eliverImmediately参数值传YES

Unregistering an Observer

  • 在观察者销毁之前, 要告知通知中心不要再给这个观察者发送消息了, 不然再次发送会发生crash
  • 使用指定的removeObserver...方法, 可以移除指定类型的通知

本地通知移除观察者

[[NSNotificationCenter defaultCenter] removeObserver:self];
复制代码

分布式通知移除观察者

[[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
复制代码

Delivering Notifications To Particular Threads

通常通知中心传递通知对象所在的线程是发送通知对象的线程, 分布式通知. 与此同时, 可能有这样的需求, 需要将通知发送到特定的特定线程上,而不是通知中心. 例如一个对象正在后台线程监听来自用户界面操作的通知, 现在后台线程收到通知而不是主线程收到, 在这些场景中, 就得捕获这些线程上的通知, 然后发送到需要用到通知对象的线程上

另外一种方式重定向通知就是使用NSNotificationQueue通知队里, 保留在不正确的线程上收到的所有通知,然后在正确的线程上处理, 操作步骤如下

  • 注册通知对象
  • 当通知到达的时候, 判断当前线程是不是真正要执行处理的线程
  • 如果不是目标线程, 则将通知存储到队列, 然后向目标线程发送信号, 表明通知需要被处理
  • 目标线程收到信号, 将通知对象送队列中移除, 然后执行处理

要实现以上技术, 观察者对象需要如下实例变量

  • 一个可变数组来存储通知对象
  • 向目标线程通讯的端口(a Mach port)
  • 锁资源防止通知数组访问冲突
  • 一个值标识目标线程(an NSThread object)
@interface MyThreadedClass: NSObject <NSPortDelegate>
/* Threaded notification support. */
@property NSMutableArray *notifications;
@property NSThread *notificationThread;
@property NSLock *notificationLock;
@property NSMachPort *notificationPort;
 
- (void) setUpThreadingSupport;
- (void) handleMachMessage:(void *)msg;
- (void) processNotification:(NSNotification *)notification;
@end

复制代码

初始化

- (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];
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort
            forMode:(NSString __bridge *)kCFRunLoopCommonModes];
}
复制代码

在初始化方法结束后, 发送到notificationPort的所有消息都会在首先运行此方法的线程的运行循环中接收到, 接收通知线程run loop在notificationPort收到消息后开始运行, 内核会一直保留该消息,直到下次进入运行循环, 接收线程的运行循环将传入消息发送到端口委托的handleMachMessage:方法

线程run loop接收到来自端口的消息, 执行端口的代理方法


- (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 {
        // Process the notification here;
    }
}

复制代码

当Mach消息到达时, handleMachMessage:方法将忽略消息的内容,仅检查通知数组中是否有需要处理的通知

因为如果同时发送太多端口信息, 消息可能会被丢弃,那么handleMachMessage:方法会在数组上迭代直到其为空

在当前线程注册观察者

[self setupThreadingSupport];
[[NSNotificationCenter defaultCenter]
        addObserver:self
        selector:@selector(processNotification:)
        name:@"NotificationName"
        object:nil];
复制代码

以上的实现方式也有一定的局限性

  • 所有线程过来的通知, 都只能通过processNotification:方法处理
  • 每个观察者都要实现自己的通信端口
  • 更好但更复杂的方式是创建NSNotificationCenter子类对象或是其他类对象, 会给每一个线程分配一个通知队列, 能够将通知对象发送到多个观察者和方法

总结

  • 其他线程Y发送了一个通知NSNotification******
  • 在本线程X想要监听观察通知
  • 其他线程发送通知是同步的, 也会在其他线程收到通知, 收到通知的时候判断, 收到通知所在的线程是不是目标线程(X线程)
  • 不是目标线程, 就朝目标线程run loop添加的端口notificationPort发送消息
  • 目标线程在本线程处理端口信息, 实际上是处理来自其他线程的通知

使用场景

想到的使用场景是: 主线程post一些需要在后台线程处理的任务通知, 让后台的常驻线程接收来自主线程的通知, 处理一些后台任务

理解如有错误 望指正 转载请说明出处

文章分类
iOS
文章标签