背景
转载 :http://www.cocoachina.com/ios/20160708/16987.html
iOS的notification在多线程的情况下,线程的管理非常不好控制。这个怎么理解呢?
按照官方文档的说法就是,不管你在哪个线程注册了 observer,notification 在哪个线程 post,那么它就将在哪个线程接收,这个意思用代码表示,效果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
- (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]);
}
|
输出如下:
1
2
3
|
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
)}</nsthread: 0x7f854845b790></nsthread: 0x7f8548405250>
|
也就是说,尽管我在主线程注册了 observer,但是由于我在子线程 post 了消息,那么 handleNotification 响应函数也会在子线程处理。这样一来就会给我们带来困扰,因为 notification 的响应函数执行线程将变得不确定,而且很多操作如 UI 操作,我们是需要在主线程进行的。
解决方案
怎么解决这个问题呢?
在响应函数处强制切线程
实现
1
2
3
4
5
6
|
一个很土的方法就在在 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 从队列中移除,并进行处理。
实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
/* 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 或者自己去提供一个单独的类进行线程的维护。
block 方式的 NSNotification
为了顺应语法的变化,apple 从 ios4 之后提供了带有 block 的 NSNotification。使用方式如下:
1
2
3
4
|
- (id<nsobject>)addObserverForName:(NSString *)name
object:(id)obj
queue:(NSOperationQueue *)queue
usingBlock:(void (^)(NSNotification *note))block</nsobject>
|
这里说明几点:
-
观察者就是当前对象
-
queue 定义了 block 执行的线程,nil 则表示 block 的执行线程和发通知在同一个线程
-
block 就是相应通知的处理函数
这个 API 已经能够让我们方便的控制通知的线程切换。但是,这里有个问题需要注意。就是其 remove 操作。
首先回忆一下我们原来的 NSNotification 的 remove 方式,见如下代码:
1
2
3
|
- (void)removeObservers {
[[NSNotificationCenter defaultCenter] removeObserver:self name:POST_NOTIFICATION object:nil];
}
|
需要指定 observer 以及 name。但是带 block 方式的 remove 便不能像上面这样处理了。其方式如下:
1
2
3
4
5
|
- (void)removeObservers {
if
(_observer){
[[NSNotificationCenter defaultCenter] removeObserver:_observer];
}
}
|
其中 _observer 是 addObserverForName 方式的 api 返回观察者对象。这也就意味着,你需要为每一个观察者记录一个成员对象,然后在 remove 的时候依次删除。试想一下,你如果需要 10 个观察者,则需要记录 10 个成员对象,这个想想就是很麻烦,而且它还不能够方便的指定 observer 。因此,理想的做法就是自己再做一层封装,将这些细节封装起来。
LRNotificationObserver
git 上有一个想要解决上述问题的开源代码,其使用方式如下:
1
2
3
4
|
+ (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 的地方移除。
GYNotificationCenter
-
为了解决上面的问题,因此决定重新写一个 Notification 的管理类,GYNotificationCenter 想要达到的效果有两个
-
能够方便的控制线程切换
-
能够方便的remove observer
使用
1
2
3
4
|
- (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 .
1
2
3
4
|
- (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 注册观察者。
1
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:anName object:object];
|
当收到通知的时候,在 handleNotification 里面执行传入的 block,回调的外面去。
1
2
3
4
5
6
7
8
9
10
11
|
- (void)handleNotification:(NSNotification *)notification {
if
(self.dispatchQueue) {
dispatch_async(self.dispatchQueue, ^{
if
(self.block) {
self.block(notification);
}
});
}
else
{
self.block(notification);
}
}
|
GYNotificationOberverIdentifer 对象放入 GYNotificationOberverIdentifersContainer 对象中进行统一管理。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
- (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 了。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
- (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];
}
}];
});
}
}
|