前幾天與同事討論到Notification在多線程下的轉(zhuǎn)發(fā)問題,所以就此整理一下墩莫。
先來看看官方的文檔宾尚,是這樣寫的:
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é)果如下:
2016-02-13 22:05:12.856 test[865:45102] current thread = {number = 1, name = main}
2016-02-13 22:05:12.857 test[865:45174] current thread = {number = 2, name = (null)}
2016-02-13 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)行后柜思,其輸出如下:
2016-02-13 23:38:31.637 test[1474:92483] current thread = {number = 1, name = main}
2016-02-13 23:38:31.663 test[1474:92483] current thread = {number = 1, name = main}
2016-02-13 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è)通知。簡單明了久信,如果這一切都是發(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ā)的線程安全問題
#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é)果:
2016-02-13 00:31:41.286 SKTest[932:88791] handle notification begin
2016-02-13 00:31:41.291 SKTest[932:88713] Observer dealloc
2016-02-13 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ā)生了什么事。我們簡要描述一下:
- 當(dāng)我們注冊(cè)一個(gè)觀察者是恒界,通知中心會(huì)持有觀察者的一個(gè)弱引用睦刃,來確保觀察者是可用的。
- 主線程調(diào)用dealloc操作會(huì)讓Observer對(duì)象的引用計(jì)數(shù)減為0十酣,這時(shí)對(duì)象會(huì)被釋放掉涩拙。
- 后臺(tái)線程發(fā)送一個(gè)通知,如果此時(shí)Observer還未被釋放耸采,則會(huì)用其轉(zhuǎn)出消息兴泥,并執(zhí)行回調(diào)方法。而如果在回調(diào)執(zhí)行的過程中對(duì)象被釋放了虾宇,就會(huì)出現(xiàn)上面的問題搓彻。
當(dāng)然,上面這個(gè)例子是故意而為之嘱朽,但不排除在實(shí)際編碼中會(huì)遇到類似的問題旭贬。雖然NSNotificationCenter是線程安全的,但并不意味著我們?cè)谑褂脮r(shí)就可以保證線程安全的搪泳,如果稍不注意稀轨,還是會(huì)出現(xiàn)線程問題。
那我們?cè)撛趺醋瞿匕毒窟@里有一些好的建議:
- 盡量在一個(gè)線程中處理通知相關(guān)的操作奋刽,大部分情況下瓦侮,這樣做都能確保通知的正常工作。不過佣谐,我們無法確定到底會(huì)在哪個(gè)線程中調(diào)用dealloc方法肚吏,所以這一點(diǎn)還是比較困難。
- 注冊(cè)監(jiān)聽都時(shí)狭魂,使用基于block的API罚攀。這樣我們?cè)赽lock還要繼續(xù)調(diào)用self的屬性或方法招刨,就可以通過weak-strong的方式來處理。具體大家可以改造下上面的代碼試試是什么效果寇仓。
- 使用帶有安全生命周期的對(duì)象又官,這一點(diǎn)對(duì)象單例對(duì)象來說再合適不過了,在應(yīng)用的整個(gè)生命周期都不會(huì)被釋放检柬。
- 使用代理。
小結(jié)
NSNotificationCenter雖然是線程安全的,但不要被這個(gè)事實(shí)所誤導(dǎo)任柜。在涉及到多線程時(shí),我們還是需要多加小心沛厨,避免出現(xiàn)上面的線程問題宙地。想進(jìn)一步了解的話,可以查看Observers and Thread Safety逆皮。