近期接觸項(xiàng)目中用的通知比較多,對(duì)于通知有一個(gè)系統(tǒng)的理解與學(xué)習(xí)清蚀,已下是做的一些總結(jié)
先來看看官方的文檔,是這樣寫的:
In a multithreaded application, notifications are always delivered in the thread in which the notification was posted, which may not be the same thread in which an observer registered itself.
翻譯過來是:
在多線程應(yīng)用中存淫,Notification在哪個(gè)線程中post艾岂,就在哪個(gè)線程中被轉(zhuǎn)發(fā),而不一定是在注冊(cè)觀察者的那個(gè)線程中潭兽。
也就是說琳骡,Notification的發(fā)送與接收處理都是在同一個(gè)線程中。為了說明這一點(diǎn)讼溺,我們先來看一個(gè)示例:
代碼清單1:Notification的發(fā)送與處理
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"current thread = %@", [NSThread currentThread]);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil];
});
}
- (void)handleNotification:(NSNotification *)notification
{
NSLog(@"current thread = %@", [NSThread currentThread]);
NSLog(@"test notification");
}
@end
其輸出結(jié)果如下:
2015-03-11 22:05:12.856 test[865:45102] current thread ={number = 1, name = main}2015-03-11 22:05:12.857 test[865:45174] current thread ={number = 2, name = (null)}
2015-03-11 22:05:12.857 test[865:45174] test notification
可以看到楣号,雖然我們?cè)谥骶€程中注冊(cè)了通知的觀察者,但在全局隊(duì)列中post的Notification怒坯,并不是在主線程處理的炫狱。所以,這時(shí)候就需要注意剔猿,如果我們想在回調(diào)中處理與UI相關(guān)的操作视译,需要確保是在主線程中執(zhí)行回調(diào)。
這時(shí)归敬,就有一個(gè)問題了酷含,如果我們的Notification是在二級(jí)線程中post的,如何能在主線程中對(duì)這個(gè)Notification進(jìn)行處理呢汪茧?或者換個(gè)提法椅亚,如果我們希望一個(gè)Notification的post線程與轉(zhuǎn)發(fā)線程不是同一個(gè)線程,應(yīng)該怎么辦呢舱污?我們看看官方文檔是怎么說的:
For example, if an object running in a background thread is listening for notifications from the user interface, such as a window closing, you would like to receive the notifications in the background thread instead of the main thread. In these cases, you must capture the notifications as they are delivered on the default thread and redirect them to the appropriate thread.
這里講到了“重定向”呀舔,就是我們?cè)贜otification所在的默認(rèn)線程中捕獲這些分發(fā)的通知,然后將其重定向到指定的線程中扩灯。
一種重定向的實(shí)現(xiàn)思路是自定義一個(gè)通知隊(duì)列(注意媚赖,不是NSNotificationQueue對(duì)象霜瘪,而是一個(gè)數(shù)組),讓這個(gè)隊(duì)列去維護(hù)那些我們需要重定向的Notification惧磺。我們?nèi)匀皇窍衿匠R粯尤プ?cè)一個(gè)通知的觀察者颖对,當(dāng)Notification來了時(shí),先看看post這個(gè)Notification的線程是不是我們所期望的線程磨隘,如果不是惜互,則將這個(gè)Notification存儲(chǔ)到我們的隊(duì)列中,并發(fā)送一個(gè)信號(hào)(signal)到期望的線程中琳拭,來告訴這個(gè)線程需要處理一個(gè)Notification训堆。指定的線程在收到信號(hào)后,將Notification從隊(duì)列中移除白嘁,并進(jìn)行處理坑鱼。
官方文檔已經(jīng)給出了示例代碼,在此借用一下絮缅,以測(cè)試實(shí)際結(jié)果:
代碼清單2:在不同線程中post和轉(zhuǎn)發(fā)一個(gè)Notification
@interface ViewController ()@property (nonatomic) NSMutableArray? ? *notifications;? ? ? ? // 通知隊(duì)列
@property (nonatomic) NSThread? ? ? ? ? *notificationThread;? ? // 期望線程
@property (nonatomic) NSLock? ? ? ? ? ? *notificationLock;? ? ? // 用于對(duì)通知隊(duì)列加鎖的鎖對(duì)象鲁沥,避免線程沖突
@property (nonatomic) NSMachPort? ? ? ? *notificationPort;? ? ? // 用于向期望線程發(fā)送信號(hào)的通信端口
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"current thread = %@", [NSThread currentThread]);
// 初始化
self.notifications = [[NSMutableArray alloc] init];
self.notificationLock = [[NSLock alloc] init];
self.notificationThread = [NSThread currentThread];
self.notificationPort = [[NSMachPort alloc] init];
self.notificationPort.delegate = self;
// 往當(dāng)前線程的run loop添加端口源
// 當(dāng)Mach消息到達(dá)而接收線程的run loop沒有運(yùn)行時(shí),則內(nèi)核會(huì)保存這條消息耕魄,直到下一次進(jìn)入run loop
[[NSRunLoop currentRunLoop] addPort:self.notificationPort
forMode:(__bridge NSString *)kCFRunLoopCommonModes];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"TestNotification" object:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil];
});
}
- (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");
}
}
@end
運(yùn)行后画恰,其輸出如下:
2015-03-11 23:38:31.637 test[1474:92483] current thread ={number = 1, name = main}
2015-03-11 23:38:31.663 test[1474:92483] current thread ={number = 1, name = main}
2015-03-11 23:38:31.663 test[1474:92483] process notification
可以看到,我們?cè)谌謉ispatch隊(duì)列中拋出的Notification吸奴,如愿地在主線程中接收到了允扇。
這種實(shí)現(xiàn)方式的具體解析及其局限性大家可以參考官方文檔Delivering Notifications To Particular Threads,在此不多做解釋则奥。當(dāng)然考润,更好的方法可能是我們自己去子類化一個(gè)NSNotificationCenter,或者單獨(dú)寫一個(gè)類來處理這種轉(zhuǎn)發(fā)读处。
NSNotificationCenter的線程安全性
蘋果之所以采取通知中心在同一個(gè)線程中post和轉(zhuǎn)發(fā)同一消息這一策略糊治,應(yīng)該是出于線程安全的角度來考量的。官方文檔告訴我們罚舱,NSNotificationCenter是一個(gè)線程安全類井辜,我們可以在多線程環(huán)境下使用同一個(gè)NSNotificationCenter對(duì)象而不需要加鎖。原文在Threading Programming Guide中管闷,具體如下:
The following classes and functions are generally considered to be thread-safe. You can use the same instance from multiple threads without first acquiring a lock.
NSArray
...
NSNotification
NSNotificationCenter
...
我們可以在任何線程中添加/刪除通知的觀察者粥脚,也可以在任何線程中post一個(gè)通知。
NSNotificationCenter在線程安全性方面已經(jīng)做了不少工作了渐北,那是否意味著我們可以高枕無憂了呢阿逃?再回過頭來看看第一個(gè)例子铭拧,我們稍微改造一下赃蛛,一點(diǎn)一點(diǎn)來:
代碼清單3:NSNotificationCenter的通用模式
@interface Observer : NSObject
@end
@implementation Observer
- (instancetype)init
{
self = [super init];
if (self)
{
_poster = [[Poster alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil]
}
return self;
}
- (void)handleNotification:(NSNotification *)notification
{
NSLog(@"handle notification ");
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end
// 其它地方
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
```
上面的代碼就是我們通常所做的事情:添加一個(gè)通知監(jiān)聽者恃锉,定義一個(gè)回調(diào),并在所屬對(duì)象釋放時(shí)移除監(jiān)聽者呕臂;然后在程序的某個(gè)地方post一個(gè)通知破托。簡(jiǎn)單明了,如果這一切都是發(fā)生在一個(gè)線程里面歧蒋,或者至少dealloc方法是在-postNotificationName:的線程中運(yùn)行的(注意:NSNotification的post和轉(zhuǎn)發(fā)是同步的)土砂,那么都OK,沒有線程安全問題谜洽。但如果dealloc方法和-postNotificationName:方法不在同一個(gè)線程中運(yùn)行時(shí)萝映,會(huì)出現(xiàn)什么問題呢?
我們?cè)俑脑煲幌律厦娴拇a:
**代碼清單4:NSNotificationCenter引發(fā)的線程安全問題**
```objc
#pragma mark - Poster
@interface Poster : NSObject
@end
@implementation Poster
- (instancetype)init
{
self = [super init];
if (self)
{
[self performSelectorInBackground:@selector(postNotification) withObject:nil];
}
return self;
}
- (void)postNotification
{
[[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil];
}
@end
#pragma mark - Observer
@interface Observer : NSObject
{
Poster? *_poster;
}
@property (nonatomic, assign) NSInteger i;
@end
@implementation Observer
- (instancetype)init
{
self = [super init];
if (self)
{
_poster = [[Poster alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:TEST_NOTIFICATION object:nil];
}
return self;
}
- (void)handleNotification:(NSNotification *)notification
{
NSLog(@"handle notification begin");
sleep(1);
NSLog(@"handle notification end");
self.i = 10;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
NSLog(@"Observer dealloc");
}
@end
#pragma mark - ViewController
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__autoreleasing Observer *observer = [[Observer alloc] init];
}
@end
這段代碼是在主線程添加了一個(gè)TEST_NOTIFICATION通知的監(jiān)聽者阐虚,并在主線程中將其移除序臂,而我們的NSNotification是在后臺(tái)線程中post的。在通知處理函數(shù)中实束,我們讓回調(diào)所在的線程睡眠1秒鐘奥秆,然后再去設(shè)置屬性i值。這時(shí)會(huì)發(fā)生什么呢咸灿?我們先來看看輸出結(jié)果:
2015-03-14 00:31:41.286 SKTest[932:88791] handle notification begin
2015-03-14 00:31:41.291 SKTest[932:88713] Observer dealloc
2015-03-14 00:31:42.361 SKTest[932:88791] handle notification end
(lldb)
// 程序在self.i = 10處拋出了"Thread 6: EXC_BAD_ACCESS(code=EXC_I386_GPFLT)"
經(jīng)典的內(nèi)存錯(cuò)誤构订,程序崩潰了。其實(shí)從輸出結(jié)果中避矢,我們就可以看到到底是發(fā)生了什么事悼瘾。我們簡(jiǎn)要描述一下:
1、當(dāng)我們注冊(cè)一個(gè)觀察者是审胸,通知中心會(huì)持有觀察者的一個(gè)弱引用分尸,來確保觀察者是可用的。
2歹嘹、主線程調(diào)用dealloc操作會(huì)讓Observer對(duì)象的引用計(jì)數(shù)減為0箩绍,這時(shí)對(duì)象會(huì)被釋放掉。
3尺上、后臺(tái)線程發(fā)送一個(gè)通知材蛛,如果此時(shí)Observer還未被釋放,則會(huì)向其轉(zhuǎn)發(fā)消息怎抛,并執(zhí)行回調(diào)方法卑吭。而如果在回調(diào)執(zhí)行的過程中對(duì)象被釋放了,就會(huì)出現(xiàn)上面的問題马绝。
4豆赏、使用代理。
小結(jié)
NSNotificationCenter雖然是線程安全的,但不要被這個(gè)事實(shí)所誤導(dǎo)掷邦。在涉及到多線程時(shí)白胀,我們還是需要多加小心,避免出現(xiàn)上面的線程問題抚岗。想進(jìn)一步了解的話或杠,可以查看Observers and Thread Safety。