通知
每个应用都有一个通知中心(NSNotificationCenter)实例,专门负责协助不同对象之间的消息通信;任何一个对象都可以向通知中心发布通知来描述自己在做什么。其他感兴趣的对象可以申请在某个特定通知发布时接收这个通知;
把一个事件发送给多个监听该事件的对象,而消息的发送者不需要知道消息接收者的任何信息,消息的接受者也只是向通知中心(NSNotificationCenter
)注册监听自己感兴趣的通知即可,任何对象都可以监听或者发送通知,这在很大程度上降低了消息发送者和接受者之间的耦合度。
通知就是以Notification的形式从通知发送者发出到通知中心,然后再由通知中心分发给所有监听该通知的对象的,通知监听者们接收到通知之后,可以获取到传递过来的Notification对象,从而获取里面封装的一些信息,做相应的处理。
NSNotificationCenter(通知中心)
通知中心是整个通知机制的关键所在,它管理着监听者的注册和注销,通知的发送和接收。通知中心维护着一个通知的分发表,把所有发送者发送的通知,转发给对应的监听者们。每一个iOS程序都有一个唯一的通知中心,它是一个单例,通过[NSNotificationCenter defaultCenter]
方法获取。
注册监听者方法:
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;
- (id <NSObject>)addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;
第一个方法是大家常用的方法,不用多说,第二个方法带了一个block,这个block就是通知被触发时要执行的block,这个block带有一个notification参数;该方法还有一个queue参数,可以指定这个block在哪个队列上执行,如果传nil,这个block将会在发送通知的线程中同步执行。这个方法有一个id类型的返回值,这个返回值是一个不透明的中间值,用来充当监听者,使用时,我们需要将这个返回的监听者保存起来,在后面移除监听者的时候用到。
还有2点需要注意的是:
- 通知中心默认是以同步的方式发送通知的;即,当一个对象发送了一个通知,只有当该通知的所有接受者都接受到了通知中心分发的通知消息并且处理完成后,发送通知的对象才能继续执行接下来的方法。异步发送通知的方法下面会说到。
- 在一个多线程的程序中,发送方发送通知的线程通常就是监听者接受通知的线程,这可能和监听者注册时的线程不一样。
Cocoa中有2中通知中心,一种是NSNotificationCenter,它只能处理一个程序内部的通知,另一种是NSDistributedNotificationCenter(mas OS上才有),它能处理一台机器上不同程序之间的通知。
NSNotificationQueue(通知队列)
通知队列就是存放通知(Notification对象)的队列。一般的发送通知方式,通知中心收到发送者发出的通知后,会立刻分发给监听者,但是如果把通知放在通知队列中,通知就可以等到某些特定时刻再发出;比如,等之前发出的通知在runloop中处理完,或runloop空闲时。它就像通知中心的缓冲池,把一些不着急发出的通知存在通知队列中。
存储在通知队列中的通知会以先进先出的方法发出(FIFO),放一个通知到达队列的头部,它将被通知队列转发给通知中心,然后通知中心再分发给相应的监听者们。
每个线程有一个默认的通知队列,它和通知中心关联着,你也可以自己为线程或者通知中心创建多个通知队列。
通知队列给通知机制提供了2个重要的特性:通知合并和异步发送通知
通知合并:
有时,对一个可能发生多次的事件,你想发送一个通知去通知某些对象做一些事,但当这个事件重复发生时,你又不想再发送同样的通知了。
你可能会这样做,设置一个flag来决定是否还需要发送通知,当第一个通知发出去时,把这个flag设置为不在需要发送通知,那么当相同的事件再发生时,就不会发送相同的通知了,看起来很美好,不过这样是不能达到我们的目的的,还是那个问题,因为普通的通知发送方式默认是同步的,通知的发送者需要等到所有的监听者都接收并处理完消息才能接着处理接下来的业务逻辑,也就是说当第一个通知发出的时候,可能还没回来,第二个通知已经发出去了,在你改变flag的值的时候,可能已经发出去若干个通知了...
这时候,就需要用到通知队列的通知合并功能了。使用NSNotificationQueue
的enqueueNotification:postingStyle:coalesceMask:forModes:
方法,设置第三个参数coalesceMask的值,来指定不同的合并规则,coalesceMask有3个给定的值:
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
NSNotificationNoCoalescing = 0,
NSNotificationCoalescingOnName = 1,
NSNotificationCoalescingOnSender = 2
};
分别是不合并,按通知的名字合并,和按通知的发送者合并。
设置合并规则后再加入到通知队列中,通知队列会按照给定的合并规则,在之前入队的通知中查找,然后移除符合合并规则的通知,这样就达到了只发送一个通知的目的。
合并规则还可以用|
符号连接,指定多个:
NSNotification *myNotification = [NSNotification notificationWithName:@"MyNotificationName" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification:myNotification
postingStyle:NSPostWhenIdle
coalesceMask:NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender
forModes:nil];
异步发送通知
使用通知队列的下面2个方法,将通知加到通知队列中,就可以将一个通知异步的发送到当前的线程,这些方法调用后会立即返回,不用再等待通知的所有监听者都接收并处理完。
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSString *> *)modes;
注意:如果通知入队的线程在该通知被通知队列发送到通知中心之前结束了,那么这个通知将不会被发送了。
注意到上面第二个方法中,有一个modes
参数,当指定了某种特定runloop mode后,该通知值有在当前runloop为指定mode的下,才会被发出。
通知队列发送通知有3种类型,也就是上面方法中的postingStyle
参数,它有3种取值:
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1,
NSPostASAP = 2,
NSPostNow = 3
};
- NSPostASAP (尽快发送 Posting As Soon As Possible)
- 以NSPostASAP风格进入队列的通知会在运行循环的当前迭代完成时被发送给通知中心,如果当前运行循环模式和请求的模式相匹配的话(如果请求的模式和当前模式不同,则通知在进入请求的模式时被发出)。由于运行循环在每个迭代过程中可能进行多个调用分支(callout),所以在当前调用分支退出及控制权返回运行循环时,通知可能被分发,也可能不被分发。其它的调用分支可能先发生,比如定时器或由其它源触发了事件,或者其它异步的通知被分发了。
开发者通常可以将NSPostASAP风格用于开销昂贵的资源,比如显示服务器。如果在运行循环的一个调用分支过程中有很多客户代码在窗口缓冲区中进行描画,在每次描画之后将缓冲区的内容刷新到显示服务器的开销是很昂贵的。在这种情况下,每个draw...方法都会将诸如“FlushTheServer” 这样的通知排入队列,并指定按名称和对象进行合并,以及使用NSPostASAP风格。结果,在运行循环的最后,那些通知中只有一个被派发,而窗口缓冲区也只被刷新一次。
- NSPostWhenIdle(空闲时发送)
- 以NSPostWhenIdle风格进入队列的通知只在运行循环处于等待状态时才被发出。在这种状态下,运行循环的输入通道中没有任何事件,包括定时器和异步事件。以NSPostWhenIdle风格进入队列的一个典型的例子是当用户键入文本、而程序的其它地方需要显示文本字节长度的时候。在用户输入每一个字符后都对文本输入框的尺寸进行更新的开销是很大的(而且不是特别有用),特别是当用户快速输入的时候。在这种情况下,Cocoa会在每个字符键入之后,将诸如“ChangeTheDisplayedSize”这样的通知进行排队,同时把合并开关打开,并使用NSPostWhenIdle风格。当用户停止输入的时候,队列中只有一个“ChangeTheDisplayedSize”通知(由于合并的原因)会在运行循环进入等待状态时被发出,显示部分也因此被刷新。请注意,运行循环即将退出(当所有的输入通道都过时的时候,会发生这种情况)时并不处于等待状态,因此也不会发出通知。
- NSPostNow(立即发送)
- 以NSPostNow风格进入队列的通知会在合并之后,立即发送到通知中心。开发者可以在不需要异步调用行为的时候 使用NSPostNow风格(或者通过NSNotificationCenter的postNotification:方法来发送)。在很多编程环境下,我们不仅允许同步的行为,而且希望使用这种行为:即开发者希望通知中心在通知派发之后返回,以便确定观察者对象收到通知并进行了处理。当然,当开发者希望通过合并移除队列中类似的通知时,应该用enqueueNotification...方法,且使用NSPostNow风格,而不是使用postNotification:方法。
发送通知到指定线程
通知中心分发通知的线程一般就是通知的发出者发送通知的线程。但是有时候,你可能想自己决定通知发出的线程,而不是由通知中心来决定。举个栗子,在后台线程中有一个对象监听者主线程中界面上的一些变化,比如一个window的关闭或者一个按钮的点击,这时通知是在主线程中发出的,通常来说只能在主线程中接受,但是你会希望这个对象能在后台线程中接到通知,而不是主线程中。这时你就需要在这些通知本来在的线程中抓住它们,然后将它们重定向到你想要指定的线程。
一种实现思路就是实现自定义的通知队列(不是NSNotificationQueue
)去保存那些通知,然后将他们重定向到你指定的线程,大致流程就是:照常用一个对象去监听一个通知,当这个通知被触发时,监听者接受到后,判断当前线程是否为处理该通知正确的线程,如果是则处理,否则,将该通知保存到我们自定义的通知队列中,然后给目标队列发送一个信号,表明这个通知需要在目标队列中处理,目标队列接受到信号后,从通知队列中取出通知并处理。
一个完整的通知一般包含3个属性:
- (NSString *) name; // 通知的名称
- (id)object; // 通知的发布者
- (NSDictionary *)userInfo; // 通知的发布者要传递给接收者的信息内容
初始化一个通知(NSNotification)对象
+ (instancetype)notificationWithName:(NSString *)aName object:(id)anObject;
+ instancetype)notificationWithName:(NSString )aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo;
- (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo;
发布通知:通知中心(NSNotificationCenter)提供了相应的方法来发布通知:
- (void)postNotification:(NSNotification *)notification;
发布一个notification通知,可在notification对象中设置通知的名称、通知发布者、额外信息等
- (void)postNotificationName:(NSString *)aName object:(id)anObject;
发布一个名称为aName的通知,anObject为这个通知的发布者
- (void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)aUserInfo;
发布一个名称为aName的通知,anObject为通知的发布者,aUserInfo为额外信息
注册通知的监听器(Observer)
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject;
observer:监听器,即谁要接收这个通知
aSelector:收到通知后,回调监听器的这个方法,并把通知对象当做参数传入;
aName:通知的名称。若为nil,则监听器接收任意名称的通知;
anObject:通知发布者。如果为anObject和aName都为nil,则监听器能接收到所有通知;
- (id)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block;
name:通知的名称
obj:通知发布者
block:收到对应的通知时,会回调这个block
queue:决定了block在哪个操作队列中执行,若为nil,默认在当前操作队列中同步执行
取消注册的通知监听器
通知中心不会保留(retain)监听器对象,在通知中心注册的对象,必须在该对象释放前取消注册。否则,当相应的通知再次出现时,通知中心仍会向该监听器发送消息。若相应的监听器对象已经被释放,就会导致应用崩溃;
通知中心提供了相应的方法来取消注册监听器:
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(NSString *)aName object:(id)anObject;
一般在监听器销毁之前取消注册(如在监听器中加入下列代码):
- (void)dealloc {
//[super dealloc]; 非ARC中需要调用此句
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
UIDevice通知
UIDevice类提供了一个单粒对象,它代表着设备,通过它可以获得一些设备相关的信息,比如电池电量(batteryLevel)、电池状态(batteryState)、设备类型(model,比如iPod、iPhone等)、设备的系统(systemVersion)等;
通过[UIDevice currentDevice]可以获取这个单粒对象:
UIDevice对象会不间断地发布一些通知,下列是UIDevice对象所发布通知的名称常量:
UIDeviceOrientationDidChangeNotification // 设备旋转
UIDeviceBatteryStateDidChangeNotification // 电池状态改变
UIDeviceBatteryLevelDidChangeNotification // 电池电量改变
UIDeviceProximityStateDidChangeNotification // 近距离传感器(比如设备贴近了使用者的脸部)
键盘通知
监听键盘的状态,可在键盘弹出或隐藏时候做一些特定的操作;
键盘状态改变时,系统会发出一些特定的通知:
UIKeyboardWillShowNotification // 键盘即将显示
UIKeyboardDidShowNotification // 键盘显示完毕
UIKeyboardWillHideNotification // 键盘即将隐藏
UIKeyboardDidHideNotification // 键盘隐藏完毕
UIKeyboardWillChangeFrameNotification // 键盘的位置尺寸即将发生改变
UIKeyboardDidChangeFrameNotification // 键盘的位置尺寸改变完毕
系统发出键盘通知时,会附带跟键盘有关的额外信息(字典),字典常见的key如下:
UIKeyboardFrameBeginUserInfoKey // 键盘刚开始的frame
UIKeyboardFrameEndUserInfoKey // 键盘最终的frame(动画执行完毕后)
UIKeyboardAnimationDurationUserInfoKey // 键盘动画的时间
UIKeyboardAnimationCurveUserInfoKey // 键盘动画的执行节奏(快慢)
键盘键盘的第三方库:http://my.oschina.net/u/1418722/blog/384477
通知和代理的选择
共同点:利用通知和代理都能完成对象之间的通信
不同点
代理 : 一对一关系(1个对象只能告诉另1个对象发生了什么事)
通知 : 多对多关系(1个对象能告诉N个对象发生了什么事, 1个对象能得知N个对象发生了什么事)