通告
在对象间传递信息的标准方法是消息传递-即一个对象调用另一个对象的方法。然而,消息传递要求发送消息的对象知道消息的接收者,以及它可以响应什么消息。这个要求对于委托消息和其它类型的消息是可以的。有些时候,我们不希望两个对象之间具有这种紧密的耦合-特别值得注意的原因是它会把本来独立的子系统联结在一起。而且这种要求也是不切实际的,因为它需要把应用程序中很多全然不同的对象之间建立硬编码的连接。
对于不能使用标准的消息传递的场合,Cocoa提供了通告广播模型。通过通告机制,一个对象可以通知其它对象自己正在干什么。在这个意义上,通告机制类似于委托,但是它们之间的区别是很重要的。委托和通告的关键区别在于前者是一对一的通讯路径(在向外委托任务的对象和被委托的对象之间)。而通告是潜在的一对多的通讯方式-也就是一种广播。一个对象只能有一个委托,但可以有很多观察者,因为通告的接收者是未知的。对象不必知道那些观察者是什么对象。任何对象都可以间接地通过通告来观察一个事件,并通过调整自己的外观、行为、和状态来响应事件。通告是一种在应用程序中进行协调和聚合的强大机制。
通告机制是如何工作的?这在概念上相当直接。在进程中有一个称为通告中心的对象,充当通告的信息交换和广播中心。在应用程序的其它地方,需要知道某个事件的对象在通告中心进行注册,让它知道当该事件发生时,自己希望得到通知。这种场景的一个例子是当一个弹出式菜单被选择时,控制器对象需要知道这个事件,以便在用户界面上对反映这个变化。当事件发生时,处理该事件的对象向通告中心发出一个通告,然后通告中心会将它派发给所有的相关的观察者。图5-8描述了这种机制。
请注意:通告中心同步地将通告派发给它的观察者。发出通告的对象直到所有的通告被发出后,才重新获得程序的控制权。如果需要以异步的方式发送通告,必须使用通告队列(参见"通告队列")。通告队列在对特定的通告进行延迟处理,并根据某些具体的条件将类似的通告进行组合之后,才将通告发给通告中心。
任何对象都可以发出通告,也可以在通告中心注册为通告的观察者。发出通告的对象、通告中包含的对象、还有通告的观察者可以是不同的对象,也可以是同一个对象(用同一个对象作为通告的发送者和观察者有它的用处,比如用于空闲处理方面)。发送通告的对象不需要知道通告观察者的任何信息。另一方面,观察者至少需要知道通告的名称和通告对象中封装的字典的键("通告对象" 部分描述了通告对象的是有什么组成的)。
本部分包含如下主要内容:
何时以及如何使用通告
和委托一样,通告机制是实现应用程序中的对象间通讯的好工具。它使应用程序中的对象可以了解其它地方发生的改变。一般地说,一个对象注册为通告的观察者,是因为它希望在相应的事件发生后或即将发生时进行调整。举例来说,如果一个定制视图希望在窗口调整尺寸的时候改变自己的外观,则可以观察窗口对象发出的NSWindowDidResizeNotification
通告。通告也允许在对象间传递信息,因为通告中可以包含一个与事件相关的字典。
但是,通告和委托之间是不同的,这些差别也导致这两种机制应该用于不同的地方。在早些时候提到,通告模型和委托模型的主要区别在于前者是广播机制,而委托是一对一的关系。每种模型都有自己的优点,通告机制的优点如下:
-
发出通告的对象不需要知道观察者的标识。
-
应用程序并不受限于Cocoa框架声明的通告;任何类都可以声明通告,其实例可以发布通告。
-
通告并不限于应用程序内部的通讯;通过分布式通告,一个进程可以将发生的事件通知另一个进程。
但是,一对一的委托模型也有自己的优点。委托有机会通过将值返回给进行委托的对象来影响事件。另一方面,通告的观察者必须发挥更为被动的作用,在响应事件时,它只能对自身及其环境产生影响。通告方法必须具有如下形式的签名:
- (void)notificationHandlerName:(NSNotification *); |
这个要求使观察对象无法以任何直接的方式影响原来的事件。但是,委托通常可以影响进行委托的对象对事件的处理方式。而且,Application Kit对象的委托自动注册为其通告的观察者。您只需要实现框架类定义的通告方法,就可以接收通告。
在Cocoa中,通告机制并不是观察对象状态变化的唯一选择,在很多情况下甚至都不是最好的选择。Cocoa绑定技术,特别是为其提供支持的键-值观察(KVO)和键-值绑定(KVB)协议,也可以使应用程序中的对象观察其它对象性质的变化。绑定机制比通告机制更为有效。在绑定机制中,被观察对象和观察对象直接进行通讯,不需要像通告中心这样的中间对象。而且,绑定机制不会像常见的通告那样,因为处理无观察者的变化而对性能产生不利影响。
但是在某些场合中,选择通告比选择绑定更为合理。您可能希望观察事件,而不是对象性质的改变;或者,有些时候遵循KVO和KVB是不适合实际情况的,特别是当需要发送和观察的通告很少的时候。
即使在适合使用通告的场合下,您也应该知道它对性能的潜在影响。通告发出之后,最终会通过本地的通告中心同步地派发给观察对象。不管通告的发送是同步的还是异步的,这个过程都是要发生的。如果有很多观察者,或者每个观察者在处理通告时都做很多工作,您的程序就会有明显的延迟。因此,您应该小心,不要过度或低效地使用通告。最后,下面这些关于通告用法的原则应该有帮助:
-
对应用程序应该观察的通告有所选择。
-
注册通告时,要具体到通告的名称和发送的对象。
-
尽可能高效地实现处理通告的方法。
-
避免添加或移除很多观察者;通过一些“中间的”观察者将通告的结果传递给它们可以访问的对象要好得多。
通告对象
通告是一个对象,是NSNotification
的一个实例。该对象封装了与事件有关的信息,比如某个窗口获得了焦点,或者某个网络连接关闭了。当事件发生时,负责处理该事件的对象会向通告中心发出一个通告,通告中心则立刻将通告广播给所有注册的对象。
NSNotification
对象中包含一个名称、一个对象、还有一个可选的字典。名称是标识通告的标签;对象是指通告的发送者希望发给通告观察者的对象(它通常是通告发送者本身),类似于委托消息的发送者对象,通告的接收者可以向该对象查询更多的信息;字典则用于存储与事件有关的信息。
通告中心
通告中心负责发送和接受通告。它将通告通知给所有符合条件的观察者。通告信息封装在NSNotification
对象中。客户对象在通告中心中将自己注册为特定通告的观察者。当事件发生时,对象向通告中心发出相应的通告。而通告中心会向所有注册过的观察者派发消息,并将通告作为唯一的参数进行传递。通告的发送对象和观察对象有可能是同一个对象。
Cocoa框架中包含两种类型的通告中心:
请注意,NSNotificationCenter
和很多其它的Foundation类不同,不能和其在Core Foundation中对应的结构(CFNotificationCenterRef
)进行自由桥接。
NSNotificationCenter
每个任务都有一个缺省的通告中心,您可以通过NSNotificationCenter
的defaultCenter
类方法来进行访问。通告中心在单任务中处理通告。如果需要在同一个机器的不同任务之间进行通讯,可以使用分布式通告中心。
通告中心同步地将通告发送给观察者。换句话说,在发出一个通告时,在所有的观察者接收和处理完成通告之前,程序的控制权不会返回给发送者。如果需要异步发送通告,可以使用通告队列,这在"通告队列"部分中进行描述。
在多线程的应用程序中,通告总是在发送的线程中传送,这个线程可能不同于观察者注册所在的线程。
NSDistributedNotificationCenter
每个任务都有一个缺省的分布式通告中心,您可以通过NSDistributedNotificationCenter
的defaultCenter
类方法来访问。这个分布式通告中心负责处理同一个机器的不通任务之间的通告。如果需要实现不同机器上的任务间通讯,请使用分布式对象。
发送分布式通告是一个开销昂贵的操作。通告会被发送给一个系统级别的服务器,然后再分发到注册了该分布式通告的对象所在的任务中。发送通告和通告到达另一个任务之间的延迟是很大的。事实上,如果发出的通告太多,以致于充满了服务器的队列,就可能被丢弃。
分布式通告通过任务的运行循环来分发。任务必须运行在某种“常见”模式的运行循环下,比如NSDefaultRunLoopMode
模式,才能接收分布式通告。如果接收通告的任务是多线程的,则不要以通告会到达主线程作为前提。通告通常被分发到主线程的运行循环上,但是其它线程也可以接收通告。
尽管常规的通告中心允许任何对象作为通告对象(也就是通告封装的对象),分布式通告中心只支持将NSString
对象作为它的通告对象。由于发出通告的对象和通告的观察者可能位于不同的任务中,通告不能包含指向任意对象的指针。因此,分布式通告中心要求通告使用字符串作为通告对象。通告的匹配就是基于这个字符串进行的,而不是基于对象指针。
通告队列
NSNotificationQueue
对象(或者简单称为通告队列)的作用是充当通告中心(NSNotificationCenter
的实例)的缓冲区。通告队列通常以先进先出(FIFO)的顺序维护通告。当一个通告上升到队列的前面时,队列就将它发送给通告中心,通告中心随后将它派发给所有注册为观察者的对象。
每个线程都有一个缺省的通告队列,与任务的缺省通告中心相关联。图5-9展示了这种关联。您可以创建自己的通告队列,使得每个线程和通告中心有多个队列。
聚结的通告
NSNotificationQueue
类为Foundation Kit的通告机制增加了两个重要的特性:即通告的聚结和异步发送。聚结是把和刚进入队列的通告相类似的其它通告从队列中移除的过程。如果一个新的通告和已经在队列中的通告相类似,则新的通告不进入队列,而所有类似的通告(除了队列中的第一个通告以外)都被移除。然而,您不应该依赖于这个特殊的聚结行为。
您可以为enqueueNotification:postingStyle:coalesceMask:forModes:
方法的第三个参数指定如下的一或多个常量,指示简化的条件:
-
NSNotificationNoCoalescing
-
NSNotificationCoalescingOnName
-
NSNotificationCoalescingOnSender
您可以对NSNotificationCoalescingOnName
和NSNotificationCoalescingOnSender
常量进行位或操作,指示Cocoa同时使用通告名称和通告对象进行聚结。那样的话,和刚刚进入队列的通告具有相同名称和发送者对象的所有通告都会被聚结。
异步发送通告
通过NSNotificationCenter
类的postNotification:
方法及其变体,您可以将通告立即发送给通告中心。但是,这个方法的调用是同步的:即在通告发送对象可以继续执行其所在线程的工作之前,必须等待通告中心将通告派发给所有的观察者并将控制权返回。但是,您也可以通过NSNotificationQueue
的enqueueNotification:postingStyle:
和enqueueNotification:postingStyle:coalesceMask:forModes:
方法将通告放入队列,实现异步发送,在把通告放入队列之后,这些方法会立即将控制权返回给调用对象。
Cocoa根据排队方法中指定的发送风格和运行循环模式来清空通告队列和发送通告。模式参数指定在什么运行循环模式下清空队列。举例来说,如果您指定NSModalPanelRunLoopMode
模式,则通告只有当运行循环处于该模式下才会被发送。如果当前运行循环不在该模式下,通告就需要等待,直到下次运行循环进入该模式。
向通告队列发送通告可以有三种风格:NSPostASAP
、NSPostWhenIdle
、和NSPostNow
,这些风格将在接下来的部分中进行说明。
尽快发送
以NSPostASAP
风格进入队列的通告会在运行循环的当前迭代完成时被发送给通告中心,如果当前运行循环模式和请求的模式相匹配的话(如果请求的模式和当前模式不同,则通告在进入请求的模式时被发出)。由于运行循环在每个迭代过程中可能进行多个调用分支(callout),所以在当前调用分支退出及控制权返回运行循环时,通告可能被分发,也可能不被分发。其它的调用分支可能先发生,比如定时器或由其它源触发了事件,或者其它异步的通告被分发了。
您通常可以将NSPostASAP
风格用于开销昂贵的资源,比如显示服务器。如果在运行循环的一个调用分支过程中有很多客户代码在窗口缓冲区中进行描画,在每次描画之后将缓冲区的内容刷新到显示服务器的开销是很昂贵的。在这种情况下,每个draw...
方法都会将诸如“FlushTheServer” 这样的通告排入队列,并指定按名称和对象进行聚结,以及使用NSPostASAP
风格。结果,在运行循环的最后,那些通告中只有一个被派发,而窗口缓冲区也只被刷新一次。
空闲时发送
以NSPostWhenIdle
风格进入队列的通告只在运行循环处于等待状态时才被发出。在这种状态下,运行循环的输入通道中没有任何事件,包括定时器和异步事件。以NSPostWhenIdle
风格进入队列的一个典型的例子是当用户键入文本、而程序的其它地方需要显示文本字节长度的时候。在用户输入每一个字符后都对文本输入框的尺寸进行更新的开销是很大的(而且不是特别有用),特别是当用户快速输入的时候。在这种情况下,Cocoa会在每个字符键入之后,将诸如“ChangeTheDisplayedSize”这样的通告进行排队,同时把聚结开关打开,并使用NSPostWhenIdle
风格。当用户停止输入的时候,队列中只有一个“ChangeTheDisplayedSize”通告(由于聚结的原因)会在运行循环进入等待状态时被发出,显示部分也因此被刷新。请注意,运行循环即将退出(当所有的输入通道都过时的时候,会发生这种情况)时并不处于等待状态,因此也不会发出通告。
立即发送
以NSPostNow
风格进入队列的通告会在聚结之后,立即发送到通告中心。您可以在不需要异步调用行为的时候 使用NSPostNow
风格(或者通过NSNotificationCenter的postNotification:
方法来发送)。在很多编程环境下,我们不仅允许同步的行为,而且希望使用这种行为:即您希望通告中心在通告派发之后返回,以便确定观察者对象收到通告并进行了处理。当然,当您希望通过聚结移除队列中类似的通告时,应该用enqueueNotification
...方法,且使用NSPostNow
风格,而不是使用postNotification:
方法。
posted on 2012-02-11 08:14 pengyingh 阅读(1280) 评论(0) 编辑 收藏 举报