通知机制想必大家都很熟悉,平常的开发中或多或少的应该都用过。它是 Cocoa 中一个非常重要的机制,能把一个事件发送给多个监听该事件的对象,而消息的发送者不需要知道消息接收者的任何信息,消息的接受者也只是向通知中心(NSNotificationCenter)注册监听自己感兴趣的通知即可,任何对象都可以监听或者发送通知,这在很大程度上降低了消息发送者和接受者之间的耦合度。这也是 iOS 中观察者模式的一种实现方式。
Notification(通知)
当我们发通知时,我们发送的是什么?答案是 Notification,一个 Notification 对象封装了通知发送者想要传递给监听的的信息,它有3个属性:
@property (readonly, copy) NSString *name; // 通知的名称,用来标示一个通知,一般为字符串
@property (nullable, readonly, retain) id object; // 任意想要携带的对象,通常为发送者自己
@property (nullable, readonly, copy) NSDictionary *userInfo; // 附加信息
通知就是以 Notification 的形式从通知发送者发出,到通知中心,然后再分发给所有监听该通知的对象的,通知监听者们接收到通知之后,可以获取到传递过来的 Notification 对象,从而获取里面封装的一些信息,做相应的处理。
NSNotificationCenter
通知中心是整个通知机制的关键所在,它管理着监听者的注册和注销,通知的发送和接收。通知中心维护着一个通知的分发表,把所有通知发送者发送的通知,转发给对应的监听者们。每一个 iOS 程序都有一个唯一的通知中心,你不必自己去创建一个,它是一个单例,通过[NSNotificationCenter defaultCenter] 方法获取。
注册监听者方法:
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;
- (id
)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 类型的返回值,这个返回值是一个不透明的中间值,用来充当监听者,使用时,我们需要将这个返回的监听者保存起来,在后面移除监听者的时候用到。
通知移除
只要往 NSNotificationCenter 注册了,就必须有 remove 的存在,一般在 dealloc 中进行 remove。
向 NSNotificationCenter 中 addObserver 后,并没有对这个对象进行引用计数加1操作,它只是保存了地址。当这个对象销毁时如果不在通知中心中移除通知,当收到通知时,会造成向野指针对象发送消息,会 crash。
但是大家在使用的时候发现,在 UIViewController 中 addObserver 后没有移除,好像也没有挂!我们为 NSNotificationCenter 添加个类别,重写他的 - (void)removeObserver:(id)observer方法。
当 UIViewController pop 后,会调用 removeObserver 方法。证明系统的 UIViewController 在销毁的时候调用了这个方法。
多线程通知
NSNotificationCenter 消息的接受线程是基于发送消息的线程的。也就是同步的,因此,有时候,你发送的消息可能不在主线程,而大家都知道操作UI必须在主线程,不然会出现不响应的情况。所以,在你收到消息通知的时候,注意选择你要执行的线程。下面看个示例代码
接受消息通知的回调
- (void)test {
dispatch_async(dispatch_get_main_queue(), ^{
//do your UI
});
}
发送消息的线程
- (void)sendNotification {
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(defaultQueue, ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"test" object:nil];
});
}
通知中心默认是以同步的方式发送通知的,也就是说,当一个对象发送了一个通知,只有当该通知的所有接受者都接受到了通知中心分发的通知消息并且处理完成后,发送通知的对象才能继续执行接下来的方法。
在一个多线程的程序中,发送方发送通知的线程通常就是监听者接受通知的线程,这可能和监听者注册时的线程不一样。
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
*)modes;
上面的异步并不是真正的异步,只是用一个队列去管理通知的发送方式,相当于延迟发送一样。如果你发送的通知并不是做UI处理工作,而是一个异步的数据存储刷新功能,可以用下面的代码。
dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(defaultQueue, ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"test" object:nil];
});
注意:如果通知入队的线程在该通知被通知队列发送到通知中心之前结束了,那么这个通知将不会被发送了。
注意到上面第二个方法中,有一个modes参数,当指定了某种特定runloop mode后,该通知值有在当前runloop为指定mode的下,才会被发出。
通知队列发送通知有3种类型,也就是上面方法中的postingStyle参数,它有3种取值:
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
NSPostWhenIdle = 1,// 空闲时发送
NSPostASAP = 2,// 尽快发送
NSPostNow = 3// 立即发送
}