通知编程指南文档笔记Notification Programming Topics
Foundation提供了一个编程架构来传递有关事件发生的信息。这个架构本质上是对于不同线程之间传递消息,事件处理的封装,依赖于run loop运行机制,通过run loop mode传递事件,通过runloop的observer回调处理事件。
Notifications
通知封装了有关事件的信息,例如获取焦点或网络连接关闭的窗口。需要了解某个事件的对象(例如,需要知道其窗口即将关闭的文件)的对象向通知中心注册,以便在发生该事件时通知它。当事件发生时,通知会发布到通知中心,通知中心立即向所有注册对象广播通知。可选地,通知在通知队列中排队,通知队列在延迟指定的通知后将通知发送到通知中心,并根据指定的某些指定标准合并类似的通知。
通知和基本原理
在对象之间传递信息的标准方式是消息传递 – 一个对象调用另一个对象的方法。但是,消息传递要求发送消息的对象知道接收者是谁以及它响应的消息是什么。有时候,这两个对象的紧密耦合是不可取的 – 最显着的原因是它将两个独立的子系统连接在一起。对于这些情况,引入了广播模型:对象发布通知,通过NSNotificationCenter对象或通知中心将其分发给相应的观察者。
NSNotification对象(称为通知)包含名称,对象和可选字典。该名称是标识通知的标签。该对象是通知的发布者想要发送给该通知的观察者的任何对象 – 通常是发布通知本身的对象。该字典可能包含有关该事件的其他信息。
通知中心
可可包括两种类型的通知中心: NSNotificationCenter类在单个进程中管理通知。 NSDistributedNotificationCenter类管理单台计算机上多个进程间的通知。
NSNotificationCenter
每个进程都有一个默认通知中心,您可以使用NSNotificationCenter + defaultCenter类方法访问该中心。此通知中心在单个流程中处理通知。对于同一台计算机上的进程之间的通信,请使用分布式通知中心(请参阅NSDistributedNotificationCenter)。通知中心同步向观察员发送通知。换句话说,发布通知时,直到所有观察者都收到并处理了通知,控制权才会返回到发布者。要异步发送通知,请使用Notification Queues中所述的通知队列。在多线程应用程序中,通知总是在发布通知的线程中传递,可能与观察者自己注册的线程不同。
NSDistributedNotificationCenter
此feature只限macOS only,iOS SDK不支持。每个进程都有一个默认的分布式通知中心,您可以使用NSDistributedNotificationCenter + defaultCenter类方法访问该中心。这个分布式通知中心处理可以在一台机器上的进程之间发送的通知。对于不同机器上的进程间的通信,请使用分布式对象(请参阅分布式对象编程主题)。
发布分发通知是一项昂贵的操作。通知被发送到系统范围的服务器,然后将其分发到所有具有为分布式通知注册的对象的进程。发布通知和通知到达另一个流程之间的等待时间是无限的。事实上,如果发布太多通知并且服务器的队列已满,通知可能会被丢弃。
分布式通知通过进程的run loop提供。进程必须在“common” mode 之一运行run loop,例如NSDefaultRunLoopMode,以接收分布式通知。如果接收进程是多线程的,则不要依赖到达主线程的通知。通知通常会传递给主线程的run loop,但其他线程也可能会收到通知。
鉴于常规通知中心允许观察任何对象,分布式通知中心仅限于观察字符串对象。因为发布对象和观察者可能位于不同的进程中,所以通知不能包含指向任意对象的指针。因此,分布式通知中心需要通知使用字符串作为通知对象。通知匹配是基于此字符串完成的,而不是一个对象指针。
Notification Queues
Notification Queue对象,是一个notification center的buffer。NSNotificationQueue 类贡献了两个重要特性给foundation kit的通知机制:聚合通知/异步发送。
Notification Queues 基础
使用NSNotificationCenter的postNotification:方法及其变体,可以将通知发布到通知中心。当时,这个方法的调用是同步的:在posting 对象能恢复执行线程之前,他必须等待知道notification center调度这个通知到所有观察者然后返回后。另一方面,通知队列通常以先进先出(FIFO)顺序维护通知(NSNotification的实例)。 当通知上升到队列的前端时,队列将其发布到通知中心,通知中心又将通知分派到注册为观察员的所有对象。
每个线程都有一个默认通知队列,该通知队列与该进程的默认通知中心相关联。 您可以创建自己的通知队列,并且每个中心和线程都有多个队列。
异步发送通知
NSNotificationQueue的 enqueueNotification:postingStyle: 和 enqueueNotification:postingStyle:coalesceMask:forModes: 方法可以发送一个异步通知到当前线程(通过加入队列的方式)。这些方法在将通知放入队列后立即返回到调用对象。
注意:如果通知排队的线程在通知队列将通知发布到其通知中心之前终止,则不会发布通知。
通知队列已清空,并根据排队方法中指定的post样式和runloop模式发布通知。 mode参数指定队列将被清空的run loop mode。 例如,如果您指定NSModalPanelRunLoopMode,则只有在运行循环处于此模式时才会发送通知。 如果运行循环当前不处于此模式,则通知将等待,直到下一次进入该模式。
发布到通知队列可以使用三种不同样式之一:NSPostASAP,NSPostWhenIdle和NSPostNow。
NSPostASAP
假设当前run loop mode与请求的模式匹配,当运行循环的当前迭代完成时,使用NSPostASAP样式排队的所有通知将发布到通知中心。 (如果请求的和当前的模式不同,则在输入所请求的模式时发布通知。)由于run loop可以在每次迭代期间进行多次标注,因此只要当前标注退出,通知就可能传递或不传递;控制返回到运行循环。其他标注可能会首先发生,例如计时器或源触发或其他异步通知正在传递。
对于昂贵的资源(如显示服务),通常使用NSPostASAP发布。当许多客户端在run loop的调用期间绘制窗口缓冲区时,在每次绘制操作之后将缓冲区刷新到显示服务器是很昂贵的。在这种情况下,每个draw …方法通过合并指定的名称和对象以及NSPostASAP的发布样式来排队某些通知,如“FlushTheServer”。因此,只有其中一个通知在运行循环结束时分派,并且窗口缓冲区仅刷新一次。
NSPostWhenIdle
只有在运行循环处于等待状态时,才会发布使用NSPostWhenIdle样式排队的通知。在这种状态下,运行循环的输入通道中没有任何东西,无论是定时器还是其他异步事件。使用NSPostWhenIdle样式进行排队的典型示例在用户输入文本时发生,程序会在某处显示文字长度。在用户输入每个字符后更新文本字段的大小会非常昂贵(并且不是很有用),尤其是在用户输入快的情况下。在这种情况下,程序会将通知(例如“ChangeTheDisplayedSize”)打开并启用合并,并在输入每个字符后对发布样式NSPostWhenIdle进行排队。当用户停止输入时,当运行循环进入其等待状态并更新显示时,会发布队列中的单个“ChangeTheDisplayedSize”通知(由于合并)。请注意,即将退出的运行循环(所有输入通道已到期时发生)未处于等待状态,因此不会发出通知。
NSPostNow
与NSPostNow排队的通知在合并到通知中心后立即发布。当您不需要异步调用行为时,您可以使用NSPostNow对通知进行排队(或使用postNotification发布一个通知:)。对于许多编程情况,同步行为不仅是允许的,而且是可取的:您希望通知中心在分派后返回,以便确保观察对象已收到并处理了通知。当然,您应该使用enqueueNotification …和NSPostNow,而不是使用postNotification:当队列中存在类似的想要通过合并删除的通知时。
这里我们可以推断,Notification center或者noification queue 一定是监听了观察者线程的runloop obser。
合并Notifications
在某些情况下,如果某个事件至少发生一次,您可能会发布通知,但即使该事件多次发生,您也不想发布超过一条通知。例如,在接收分组数据的应用程序中,收到数据包后,您可能希望发布通知以表示数据需要处理。但是,如果多个数据包在给定的时间段内到达,则不需要发布多个通知。此外,发布这些通知的对象可能无法知道是否有更多数据包即将到来,发布方法是否在循环中调用。
在某些情况下,可以简单地设置一个布尔标志(无论是对象的实例变量还是全局变量)来表示事件已经发生,并且可以在清除标志之前抑制进一步通知的发布。但是,如果这种情况不可行,在这种情况下,由于其行为是同步的,因此无法直接使用NSNotificationCenter – 在返回之前发布通知,因此没有机会“忽略”重复通知;而且,NSNotificationCenter实例无法知道是否有更多的通知即将到来。
因此,您可以将通知添加到指定适当合并选项的NSNotificationQueue实例中,而不是将通知发布到通知中心。合并是一种从队列中删除与以前排队的通知类似的通知的过程。可以通过在enqueueNotification的第三个参数中指定一个或多个以下常量来指示相似性标准:postingStyle:coalesceMask:forModes:方法。参数选项如下:
您可以使用NSNotificationCoalescingOnName和NSNotificationCoalescingOnSender常量执行按位或运算“|”,以指定使用通知名称和通知对象进行合并。以下示例说明如何使用队列来确保在给定事件循环周期内,名为MyNotificationName的所有通知都合并为单个通知。
// 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];
|
注册一个本地的通知
通过调用通知中心方法addObserver:selector:name:object:来注册一个对象,以接收通知:指定观察者,通知中心应发送给观察者的消息,要接收的通知的名称以及有关哪个对象。您不需要指定名称和对象。如果只指定一个对象,则观察者将收到包含该对象的所有通知。如果仅指定通知名称,则观察者将在每次发布通知时收到该通知,而不管与其关联的对象如何。
观察员可能注册接收同一通知的多条消息。在这种情况下,观察者将收到所有注册接收通知的消息,但是无法确定接收消息的顺序。
如果稍后决定观察者不再需要接收通知(例如,如果观察者正在解除分配),则可以使用方法removeObserver:或removeObserver:name:object:从观察者列表中删除观察者。
通常情况下,您可以使用流程的默认通知中心注册对象。您可以使用defaultCenter类方法获取默认对象。
作为使用通知中心接收通知的示例,假设您希望在窗口成为主窗口时执行操作(例如,如果您正在为检查器面板实施控制器)。您可以将您的客户端对象注册为观察者,如下例所示:
[[NSNotificationCenter defaultCenter] addObserver:self
|
selector:@selector(aWindowBecameMain:)
|
name:NSWindowDidBecomeMainNotification object:nil];
|
通过传递nil作为要观察的对象,当任何对象发布NSWindowDidBecomeMainNotification通知时,通知客户端对象(self)。
当窗口变为主窗口时,它会将NSWindowDidBecomeMainNotification发布到通知中心。 通知中心通过调用它们在addObserver的selector参数中指定的方法通知所有对通知感兴趣的观察者:selector:name:object :. 在我们的示例观察者的情况下,调用方法是 aWindowBecameMain:方法可能具有以下实现:
– (void)aWindowBecameMain:(NSNotification *)notification {
|
NSWindow *theWindow = [notification object];
|
MyDocument = (MyDocument *)[[theWindow windowController] document];
|
// Retrieve information about the document and update the panel.
|
}
|
注册一个分布式消息通知
一个对象通过发送addObserver:selector:name:object:suspensionBehavior:方法向NSDistributedNotificationCenter对象注册,指定通知应该发送的消息,它想要接收的通知的名称,要匹配的标识字符串(对象参数)以及在通知传递被暂停时要遵循的行为。
因为发布对象和观察者可能位于不同的进程中,所以通知不能包含指向任意对象的指针。因此,NSDistributedNotificationCenter类需要通知才能将NSString对象用作对象参数。通知匹配是基于此字符串完成的,而不是一个对象指针。您应该检查发布通知的对象的文档,以查看它用作标识字符串的内容。
当流程不再有兴趣立即收到通知时,它可能会暂停通知传递。这通常在应用程序隐藏或放入后台时完成。 (当应用程序未处于活动状态时,NSApplication对象会自动暂停传递。)addObserver方法中的suspensionBehavior参数标识在传递暂停时应如何处理到达的通知。有四种不同类型的暂停行为,每种在不同情况下都有用。
Suspension Behavior | Description |
NSNotificationSuspensionBehaviorDrop | The server does not queue any notifications with this name and object until it receives the setSuspended:NO message. |
NSNotificationSuspensionBehaviorCoalesce | The server queues only the last notification of the specified name and object; earlier notifications are dropped. In cover methods for which suspension behavior is not an explicit argument, NSNotificationSuspensionBehaviorCoalesce is the default. |
NSNotificationSuspensionBehaviorHold | The server holds all matching notifications until the queue has been filled (queue size determined by the server) at which point the server may flush queued notifications. |
NSNotificationSuspensionBehaviorDeliverImmediately | The server delivers notifications matching this registration irrespective of whether it has received the setSuspended:YES message. When a notification with this suspension behavior is matched, it has the effect of first flushing any queued notifications. The effect is as if the server received setSuspended:NO while the application is suspended, followed by the notification in question being delivered, followed by a transition back to the previous suspended or unsuspended state. |
通过向分布式通知中心发送setSuspended:YES来暂停通知。在通知被挂起时,通知服务器会根据观察者在注册接收通知时指定的暂停行为来处理发往暂停通知传递的进程的通知。当进程恢复通知传递时,所有排队的通知将立即传递。在使用Application Kit的应用程序中,当应用程序未处于活动状态时,NSApplication对象会自动挂起通知传递。
请注意,发送给注册NSNotificationSuspensionBehaviorDeliver的观察者的通知立即自动刷新队列,导致所有排队的通知也在此时传递。
挂起状态可以被通知的发送者覆盖。如果通知是紧急的,例如关闭服务器的警告,则发布者可以通过将通知发布到NSDistributedNotificationCenter的postNotificationName:object:userInfo:deliverImmediately:方法中,使用deliverImmediately参数YES强制将通知立即传递给所有观察者。
发送一个local notification
您可以使用notificationWithName:object:或notificationWithName:object:userInfo:创建通知对象。然后使用postNotification:实例方法将通知对象发布到通知中心。 NSNotification对象是不可变的,所以一旦创建,它们就不能被修改。
但是,您通常不会直接创建自己的通知。 NSNotificationCenter类的方法postNotificationName:object:和postNotificationName:object:userInfo:允许您方便地发布通知,而不必先创建通知。
在每种情况下,您通常都会将通知发布到流程的默认通知中心。您可以使用defaultCenter类方法获取默认对象。
作为使用通知中心发布通知的示例,请考虑注册本地通知中的示例。您有一个程序可以对文本执行多次转换(例如,RTF到ASCII)。转换由一组对象(Converter)处理,可在程序执行期间添加或删除。你的程序可能有其他的对象,当转换器被添加或删除时想要被通知,但是Converter对象不需要知道这些对象是谁或者他们做了什么。因此,您声明了两个通知,即“ConverterAdded”和“ConverterRemoved”,您在发生给定事件时发布该通知。
当用户安装或删除转换器时,它会将以下消息之一发送到通知中心:
[[NSNotificationCenter defaultCenter]
|
postNotificationName:@”ConverterAdded” object:self];
|
or
[[NSNotificationCenter defaultCenter]
|
postNotificationName:@”ConverterRemoved” object:self];
|
Posting Distributed Notifications
和发动local notifications类似,只是分布式notification不支持发送对象指针。
向特定线程发送通知
常规通知中心在发布通知的线程上发送通知。分布式通知中心在主线程上发送通知。有时,您可能需要通过由您而不是通知中心确定的特定线程发送通知。例如,如果运行在后台线程中的对象正在侦听来自用户界面的通知(例如窗口关闭),则希望在后台线程中接收通知,而不是在主线程中收到通知。在这些情况下,您必须捕获在默认线程上传递的通知并将其重定向到适当的线程。
重定向通知的一种方式是使用自定义通知队列(不是NSNotificationQueue对象)来保存在不适当的线程上接收到的任何通知,然后在正确的线程上处理它们。该技术如下工作,您通常会注册通知,当通知到达时,您测试当前线程是否应该处理通知的线程。如果它是错误的线程,则将通知存储在队列中,然后将信号发送到正确的线程,指示需要处理通知。另一个线程接收信号,从队列中删除通知,并处理通知。
要实现这种技术,您的观察者对象需要具有以下值的实例变量:一个可变数组来保存通知,一个用于发信号通知正确线程的通信端口(一个Mach端口),一个用于防止多线程与通知数组冲突的锁,和一个标识正确线程的值(一个NSThread对象)。您还需要设置变量,处理通知以及接收Mach消息的方法。以下是添加到观察者对象的类中的必要定义。
@interface MyThreadedClass: NSObject
|
/* 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
|
在注册任何通知之前,您需要初始化属性。 下面的方法初始化队列和锁定对象,保持对当前线程对象的引用,并创建一个Mach通信端口,将它添加到当前线程的run loop中。
– (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的任何消息都将在首次运行此方法的线程的运行循环中收到。如果在Mach消息到达时接收线程的运行循环没有运行,内核会保留该消息,直到下一次输入runloop。接收线程的运行循环将传入消息发送到端口委托的handleMachMessage:方法。
在此实现中,发送到notificationPort的消息中不包含任何信息。相反,线程之间传递的信息包含在通知数组中。当Mach消息到达时,handleMachMessage:方法会忽略消息的内容,并仅检查通知数组中是否有需要处理的通知。通知将从数组中移除并转发给真正的通知处理方法。因为如果同时发送太多端口消息,则端口消息可能会丢失,handleMachMessage:方法遍历数组直到它为空。该方法在访问通知数组时必须获取锁,以防止一个线程添加通知与另一个线程从数组中删除通知之间的冲突。
– (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;
|
}
|
}
|
最后,要注册想要在当前线程上传递的通知,无论它可能在哪个线程中发布,您都必须通过调用setUpThreadingSupport来初始化对象的通知属性,然后正常注册通知,指定特殊通知处理 方法作为调用方法。
[self setupThreadingSupport];
|
[[NSNotificationCenter defaultCenter]
|
addObserver:self
|
selector:@selector(processNotification:)
|
name:@”NotificationName”
|
object:nil];
|
这个实现在几个方面是受限的。 首先,由该对象处理的所有线程通知都必须通过相同的方法(processNotification :)。 其次,每个对象都必须提供自己的实现和通信端口。 一个更好但更复杂的实现会将“行为/操作”推广到NSNotificationCenter的子类中,或将单独的类为每个线程设置一个通知队列,并且能够将通知传递给多个观察者对象和方法。