Cocoa为应用提供了一个单例的NSNotificationCenter,它的非正式名称叫作通知中心。该实例可以通过调用NSNotificationCenter.defaultCenter()来获取,它是消息发送(叫作通知)机制的基础。一个通知就是一个NSNotification实例。其想法是任何对象都可以注册到通知中心以接收某些通知。另一个对象可以向通知中心发送通知对象(叫作发布通知)。接下来,通知中心就会向所有注册以准备接收通知的对象发送该通知。
人们经常将通知机制称为分发或广播机制,这样描述的理由也很充分。凭借该机制,对象可以发送消息而不必了解或关心什么对象、多少对象会接收到该通知。这样做简化了应用的架构,使得系统不必再将实例连接起来才能实现彼此间的消息传递(这有时是很难做到的,第13章将会介绍)。当对象从概念上做到了彼此间的“隔离”,通知就是一个相当轻量级的方式,可以让一个对象向另一个对象发送消息。
一个NSNotification对象包含了3部分信息,这些信息可以通过其实例方法获取到:
name
一个NSString,表示通知的含义。
object
与通知关联的一个实例;一般来说是发送通知的实例。
userInfo
并非每个通知都有userInfo;它是个NSDictionary,可以包含与通知相关的一些附加信息,该NSDictionary到底包含什么信息,信息位于哪些键中取决于具体的通知;你需要查询文档才能获悉。比如,文档表明UIApplication的UIApplicationDidChange-StatusBarOrientationNotification包含了一个userInfo字典,字典有一个UIAppli-cationStatusBarOrientationUserInfoKey键,其值是状态栏之前的方向。在自己发布通知时,你可以将任何感兴趣的信息放到userInfo中供通知接收者获取。
Cocoa本身会通过通知中心来发送通知,你的代码可以注册到通知中心来接收通知。对于提供了通知的类的文档,你会看到有一个单独的Notifications部分来介绍它们。
11.3.1 接收通知
要想注册以接收通知,你需要向通知中心发送如下两条消息之一,一个是addObserver:selector:name:object:,其参数如下所示。
observer:
通知发向的实例。它通常是self;一般不会出现一个实例将另一个不同的实例注册为通知接收者的情况。
selector:
当通知出现时,发送给观察者实例的消息。指定的方法应该不返回结果(Void)并且带有一个参数,参数是NSNotification实例(因此,参数的类型应该是NSNotification或AnyObject)。在Swift中,可以通过将方法名作为字符串来指定选择器。
不要将方法的字符串名搞错了,也不要忘记实现方法!如果通知中心通过调用作为选择器的方法来发送通知,并且该方法不存在,那么应用就会崩溃。参见附录A了解关于如何将方法转换为字符串名的规则。
只有当选择器所命名的方法公开给了Objective-C时才能调用它。如果通知中心通过调用作为选择器的方法来发送通知,但Objective-C不知道这个方法,那么应用就会崩溃。如果类是NSObject的子类,或方法标记为@objc(或dynamic),那么Objective-C才能知道这个方法。
name:
你想要接收的通知的字符串名。如果该参数为nil,那么你就会接收到与object:参数中所指定的对象相关的所有通知。内建的Cocoa通知名通常是个常量。这是很有用的,因为如果搞乱了常量名,编译器就会报错,如果直接以字符串字面值的形式输入通知名,但却输错了,那么编译器就不会报错,但却无法收到任何通知了(因为没有与你输入的名字所对应的通知),这种错误是很难追踪的。
object:
你所感兴趣的通知对象,通常是发布通知的那个对象。如果为nil,那么你就会接收到name:参数中所指定名字的所有通知(如果name:与object:参数都为nil,那么你就会接收到所有通知)。
比如,在我的一个应用中,我希望在设备的音乐播放器开始播放下一首歌曲时界面能够变化。设备内建的音乐播放器API属于MPMusicPlayerController类;该类提供了一个通知,告诉我内建的音乐播放器何时改变了正在播放的歌曲,该通知的说明位于MPMusicPlayerController类文档中的Notifications下,名字是MPMusicPlayerController-NowPlayingItemDidChangeNotification。
查看文档会发现,只有先调用MPMusicPlayerController的beginGeneratingPlaybackNotifications实例方法后才会发送该通知。这种架构很常见;对于某些通知来说,只有开启后Cocoa才会发送,从而提升了效率。这样,我首先需要获取到MPMusicPlayerController的一个实例,然后调用该方法:
let mp = MPMusicPlayerController.systemMusicPlayermp.beginGeneratingPlaybackNotifications
现在,注册自身以接收所需的播放通知:
NSNotificationCenter.defaultCenter.addObserver(self, selector: /"nowPlayingItemChanged:/", name: MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: nil)
这样,当发布MPMusicPlayerControllerNowPlayingItemDidChangeNotification通知后,nowPlayingItemChanged:方法就会被调用:
func nowPlayingItemChanged (n:NSNotification) { self.updateNowPlayingItem // ... and so on ...}
要想让addObserver:selector:name:object:能够正常运作,你需要获取到正确的选择器,并确保实现了相应的方法。大量使用addObserver:selector:name:object:意味着代码中会充斥着大量仅供通知中心调用的方法。并没有关于这些方法的说明(你需要添加一些注释来提醒自己),同时这些方法又独立于注册调用,所有这一切使代码变得非常混乱。
可以通过另一种通知注册方式来解决这个问题,即调用addObserverForName:object:queue:usingBlock:。它会返回一个值,这个值的目的将会在后面介绍。queue:通常为nil;非nil的queue:用于后台线程。name:与object:参数就像是addObserver:selector:name:object:中相应的参数一样。相对于使用观察者与选择器,你需要提供一个Swift函数,它包含了通知到达时所要执行的实际代码。该函数接收一个参数,即NSNotification本身。如果使用了匿名函数,那么对于所注册的通知的响应就会成为注册的一部分:
let ob = NSNotificationCenter.defaultCenter .addObserverForName( MPMusicPlayerControllerNowPlayingItemDidChangeNotification, object: nil, queue: nil) { _ in self.updateNowPlayingItem // ... and so on ...}
使用addObserverForName:...会导致一些额外的内存管理问题,第12章将会对此进行介绍。
11.3.2 取消注册
对于注册为通知接收者的每个对象,你可以在其销毁之前取消注册。如果没有取消注册,对象又不存在了,同时该对象注册以接收的消息又发送出来了,那么通知中心就会尝试向该对象发送恰当的消息,现在就接收不到了。这样,最好的结果就是应用崩溃,最糟糕的结果就是出现了混乱。
要想取消注册通知接收者对象,请向通知中心发送removeObserver:消息(此外,还可以使用removeObserver:name:object:让对象取消注册特定的通知集)。作为observer:参数所传递的对象就是不再接收通知的那个对象。这个对象是什么取决于你一开始是如何注册的:
调用了addObserver:...
一开始就提供了观察者;它就是现在要取消注册的那个观察者。
调用了addObserverForName:...
对addObserverForName:...的调用会返回一个类型为NSObjectProtocol的观察者标记对象(无须关心其真实的类型与特性);它就是现在要取消注册的那个观察者。
棘手之处在于要找到恰当的时机来取消注册。靠谱的解决方案是注册实例的deinit方法,它是实例销毁前所接收到的最后一个生命周期事件。
如果调用了同一个类的addObserverForName:...多次,那就会从通知中心接收到多个观察者标记,你需要将其保存起来以便后续可以取消他们的注册。如果想一次取消注册全部对象,一种方案就是使用类型为可变集合的实例属性。我喜欢使用Set属性:
var observers = Set<NSObject>
每次使用addObserverForName...注册通知时,我都会捕获到结果并将其添加到集合中:
let ob = NSNotificationCenter.defaultCenter.addObserverForName(/*...*/)self.observers.insert(ob as! NSObject)
在取消注册时,我会枚举集合并将其清空:
for ob in self.observers { NSNotificationCenter.defaultCenter.removeObserver(ob)}self.observers.removeAll
NSNotificationCenter是无法内省的:你不能通过NSNotificationCenter获取到注册为通知接收者的对象。这是Cocoa功能的一个欠缺,如果犯了诸如过早取消某个观察者的注册这类错误,那么Bug是很难追踪的(与往常一样,这也来自于我痛苦的经历)。
11.3.3 发布通知
虽然很多时候都是从Cocoa接收通知的,不过也可以利用通知机制实现自定义对象间的通信。这么做的一个原因在于两个对象从概念上会彼此独立。不过,值得注意的是不要过多地使用通知,也不要将其作为对象间通信的链路;但在某些情况下它们还是非常适合的(第13章将会介绍)。
要想按照这种方式使用通知,对象会在通信链路中扮演两个角色。一个或多个对象会注册以接收通知,如前所述,这是通过名字、对象或二者的结合体来标识的。另一个对象会发布通知,标识方式也是一样的。接下来,通知中心会将消息从发送者传递给注册的接收者。
要想发布通知,请向通知中心发送消息postNotificationName:object:userInfo:。
比如,我曾开发过一款简单的纸牌游戏。游戏需要知道用户什么时候轻拍了纸牌,不过纸牌却对游戏一无所知;当用户轻拍纸牌时,它只是通过发布通知发出声音而已:
NSNotificationCenter.defaultCenter.postNotificationName( /"cardTapped/", object: self)
游戏对象注册过了/"cardTapped/"通知,因此它会知道这一点并接收到通知的object;现在,它知道用户轻拍了哪个纸牌,并且可以正确地进行处理。
11.3.4 NSTimer
严格来说,定时器(NSTimer)并非通知;但其行为非常类似于通知。它是一个对象,在某段时间间隔后会发出一个信号。这个信号就是发给一个实例的消息。这样,当某段时间过后,你就可以收到通知了。时间并不是非常精确的,但用起来没什么问题。
定时器管理并不难,但有点与众不同。定时器会不断检查时钟,我们称这种行为为调度。定时器可能会被触发一次,也可能是个重复定时器。要想销毁定时器,首先要将其置为无效状态。设定为只触发一次的定时器会在触发后自动变为无效状态;重复定时器会不断重复执行,直到你通过向其发送invalidate消息将其置为无效状态。你不应该再使用处于无效状态的定时器;也不能将其复活或使用它做别的事情,你也不应该向其发送任何消息。
创建定时器的直接方式是使用NSTimer类的scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:方法。这会创建定时器并对其进行调度,这样定时器就会自动开始检查时钟了。目标与选择符决定了当定时器触发时可以向什么对象发送什么消息;处理的方法应该接收一个参数,该参数是指向定时器的引用。userInfo就像是通知的userInfo一样。
对于Timer的target:与关于NSNotifications的selector:也要小心。当定时器触发时,目标一定要存在,它要有一个与动作选择器相对应的方法,Objective-C必须要能调用该方法。否则就会出问题。
NSTimer有个tolerance属性,它是个时间间隔,表示定时器可以在指定的触发时间与这个时间加上tolerance之间的某一时刻触发。文档表明可以通过为其提供一个至少为timeInterval 10%的值来改进设备电池寿命与应用响应性。
比如,我开发过一个应用,它是个游戏并且带有分数;如果用户在10秒内没有移动,那么我就要减分来惩罚用户。这样,每次用户移动时,我都会创建一个重复定时器,其时间间隔是10秒(我还会将任何现有的定时器都置为无效);在定时器调用的方法中,我会减分。
定时器存在一些内存管理问题,第12章将会介绍,此外定时器还有基于块的替代方案。