ios 的 notification 在多线程的情况下,线程的管理非常不好控制。这个怎么理解呢?
按照官方文档的说法就是,不管你在哪个线程注册了 observer,notification 在哪个线程 post,那么它就将在哪个线程接收,这个意思用代码表示,效果如下:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"current thread = %@", [NSThread currentThread]); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:POST_NOTIFICATION object:nil]; } - (void)viewDidAppear:(BOOL)animated { [self postNotificationInBackground]; } - (void)postNotificationInBackground { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:POST_NOTIFICATION object:nil userInfo:nil]; }); } - (void)handleNotification:(NSNotification *)notification { NSLog(@"current thread = %@", [NSThread currentThread]); }
输出如下:
2016-07-02 11:20:56.683 Test[31784:3602420] current thread = <NSThread: 0x7f8548405250>{number = 1, name = main} 2016-07-02 11:20:56.684 Test[31784:3602420] viewWillAppear: ViewController 2016-07-02 11:20:56.689 Test[31784:3602469] current thread = <NSThread: 0x7f854845b790>{number = 2, name = (null)}
也就是说,尽管我在主线程注册了 observer,但是由于我在子线程 post 了消息,那么 handleNotification 响应函数也会在子线程处理。这样一来就会给我们带来困扰,因为 notification 的响应函数执行线程将变得不确定,而且很多操作如 UI 操作,我们是需要在主线程进行的。
怎么解决这个问题呢?
一个很土的方法就在在 handleNotification 里面,强制切换线程,如:
- (void)handleNotification:(NSNotification *)notification { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"current thread = %@", [NSThread currentThread]); }); }
每一个响应函数都强制切换线程。这样带来的问题就是每一处理代码你都得这样做,对于开发者而言负担太大,显然是下下策。
其实解决思路和上面的差不多,不过实现的方式更优雅一点,这个方案在 apple 的官方文档 中有详细介绍,它的思路翻译过来就是:重定向通知的一种的实现思路是使用一个通知队列(注意,不是 NSNotificationQueue 对象,而是一个数组)去记录所有的被抛向非预期线程里面的通知,然后将它们重定向到预期线程。这种方案使我们仍然是像平常一样去注册一个通知的观察者,当接收到 Notification 的时候,先判断 post 出来的这个 Notification 的线程是不是我们所期望的线程,如果不是,则将这个 Notification 存储到我们自定义的队列中,并发送一个信号( signal )到期望的线程中,来告诉这个线程需要处理一个 Notification 。指定的线程在收到信号后,将 Notification 从队列中移除,并进行处理。
/* Threaded notification support. */ @property (nonatomic) NSMutableArray *notifications; // 通知队列 @property (nonatomic) NSThread *notificationThread; // 预想的处理通知的线程 @property (nonatomic) NSLock *notificationLock; // 用于对通知队列加锁的锁对象,避免线程冲突 @property (nonatomic) NSMachPort *notificationPort; // 用于向预想的处理线程发送信号的通信端口 @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; NSLog(@"current thread = %@", [NSThread currentThread]); [self setUpThreadingSupport]; // 往当前线程的run loop添加端口源 // 当Mach消息到达而接收线程的run loop没有运行时,则内核会保存这条消息,直到下一次进入run loop [[NSRunLoop currentRunLoop] addPort:self.notificationPort forMode:(__bridge NSString *)kCFRunLoopCommonModes]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:POST_NOTIFICATION object:nil]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:POST_NOTIFICATION object:nil userInfo:nil]; }); } - (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:(__bridge NSString*)kCFRunLoopCommonModes]; } - (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; NSLog(@"current thread = %@", [NSThread currentThread]); NSLog(@"process notification"); } } }
但是这种方案有明显额缺陷,官方文档也对其进行了说明,归结起来有两点:
所有的通知的处理都要经过 processNotification 函数进行处理。
所有的接听对象都要提供相应的 NSMachPort 对象,进行消息转发。
正是由于存在这样的缺陷,因此官方文档并不建议直接这样使用,而是鼓励开发者去继承NSNoticationCenter 或者自己去提供一个单独的类进行线程的维护。
为了顺应语法的变化,apple 从 ios4 之后提供了带有 block 的 NSNotification。使用方式如下:
- (id<NSObject>)addObserverForName:(NSString *)name object:(id)obj queue:(NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
这里说明几点
观察者就是当前对象
queue 定义了 block 执行的线程,nil 则表示 block 的执行线程和发通知在同一个线程
block 就是相应通知的处理函数
这个 API 已经能够让我们方便的控制通知的线程切换。但是,这里有个问题需要注意。就是其 remove 操作。
首先回忆一下我们原来的 NSNotification 的 remove 方式,见如下代码:
- (void)removeObservers { [[NSNotificationCenter defaultCenter] removeObserver:self name:POST_NOTIFICATION object:nil]; }
需要指定 observer 以及 name。但是带 block 方式的 remove 便不能像上面这样处理了。其方式如下:
- (void)removeObservers { if(_observer){ [[NSNotificationCenter defaultCenter] removeObserver:_observer]; } }
其中 _observer 是 addObserverForName 方式的 api 返回观察者对象。这也就意味着,你需要为每一个观察者记录一个成员对象,然后在 remove 的时候依次删除。试想一下,你如果需要 10 个观察者,则需要记录 10 个成员对象,这个想想就是很麻烦,而且它还不能够方便的指定 observer 。因此,理想的做法就是自己再做一层封装,将这些细节封装起来。
git 上有一个想要解决上述问题的开源代码,其使用方式如下:
+ (void)observeName:(NSString *)name owner:(id)owner dispatchQueue:(dispatch_queue_t)dispatchQueue block:(LRNotificationObserverBlock)block;
它能够方便的控制线程切换,而且它还能做到 owner dealloc 的时候,自动 remove observer。比如我们很多时候在 viewDidLoad 的时候addObserver,然后还需要重载 dealloc,在里面调用 removeObserver,这个开源方案,帮我们省去了再去dealloc 显示 remove 的额外工作。但是如果你想显式的调用 remove,就比较麻烦了(比如有时候,我们在viewWillAppear 添加了 observer,需要在 viewWillDisAppear 移除 observer),它类似官方的解决方案,需要你用成员变量,将 observer 一个个保存下来,然后在 remove 的地方移除。
为了解决上面的问题,因此决定重新写一个 Notification 的管理类, GYNotificationCenter 想要达到的效果有两个
能够方便的控制线程切换
能够方便的remove observer
- (void)addObserver:(nonnull id)observer name:(nonnull NSString *)aName dispatchQueue:(nullable dispatch_queue_t)disPatchQueue block:(nonnull GYNotificatioObserverBlock)block;
我们提供了和官方 api 几乎一样的调用方法,支持传入 dispatchQueue 实现线程切换控制,同时能够以 block 的方式处理消息响应,而且支持在 observer dealloc 的时候,自动调用 observer 的 remove 操作。同时还提供了和原生一样的显式调用 remove 的操作,方便收到调用 remove .
- (void)removerObserver:(nonnull id)observer name:(nonnull NSString *)anName object:(nullable id)anObject; - (void)removerObserver:(nonnull id)observer;
能够方便的手动调用 remove 操作。
GYNotificaionCenter 借鉴了官方的线程重定向 以及 LRNotificationObserver 的一些方案。在 addObserver 的时候,生成了一个和 observer 关联的 GYNotificationOberverIdentifer 对象,这个对象记录了传入的 block 、name 的数据,然后对这个对象依据传入的 name 注册观察者。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:anName object:object];
当收到通知的时候,在 handleNotification 里面执行传入的 block,回调的外面去。
- (void)handleNotification:(NSNotification *)notification { if (self.dispatchQueue) { dispatch_async(self.dispatchQueue, ^{ if (self.block) { self.block(notification); } }); } else { self.block(notification); } }
GYNotificationOberverIdentifer 对象放入 GYNotificationOberverIdentifersContainer 对象中进行统一管理。
- (void)addNotificationOberverIdentifer:(GYNotificationOberverIdentifer *)identifier { NSAssert(identifier,@"identifier is nil"); if (identifier) { NotificationPerformLocked(^{ [self modifyContainer:^(NSMutableDictionary *notificationOberverIdentifersDic) { //不重复add observer if (![notificationOberverIdentifersDic objectForKey:identifier.name]) { [notificationOberverIdentifersDic setObject:identifier forKey:identifier.name]; } }]; }); } }
这个对象也和 observer 关联。由于其和 observer 是关联的,因此当 observer 释放的时候,GYNotificationOberverIdentifer 也会释放,因此,也就能在 GYNotificationOberverIdentifer 的 dealloc 里面调用 remove 操作移除通知注册从而实现自动 remove。
同时由于 GYNotificationOberverIdentifersContainer 里面保留了所有的 Identifer 对象,因此也就能够方便的根据 name 进行 remove 了。
- (void)removeObserverWithName:(NSString *)name { if (name) { NotificationPerformLocked(^{ [self modifyContainer:^(NSMutableDictionary *notificationOberverIdentifersDic) { if ([notificationOberverIdentifersDic objectForKey:name]) { GYNotificationOberverIdentifer *identifier = (GYNotificationOberverIdentifer *)[notificationOberverIdentifersDic objectForKey:name]; [identifier stopObserver]; [notificationOberverIdentifersDic removeObjectForKey:name]; } }]; }); } }