Notification與多線程

近期接觸項(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。

原創(chuàng)作者

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宣蔚,一起剝皮案震驚了整個(gè)濱河市向抢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌胚委,老刑警劉巖挟鸠,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異亩冬,居然都是意外死亡兄猩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門鉴未,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枢冤,“玉大人,你說我怎么就攤上這事铜秆⊙驼妫” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵连茧,是天一觀的道長(zhǎng)核蘸。 經(jīng)常有香客問我,道長(zhǎng)啸驯,這世上最難降的妖魔是什么客扎? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮罚斗,結(jié)果婚禮上徙鱼,老公的妹妹穿的比我還像新娘。我一直安慰自己针姿,他們只是感情好袱吆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著距淫,像睡著了一般绞绒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上榕暇,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天蓬衡,我揣著相機(jī)與錄音喻杈,去河邊找鬼。 笑死狰晚,一個(gè)胖子當(dāng)著我的面吹牛筒饰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播家肯,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼龄砰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼盟猖!你這毒婦竟也來了讨衣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤式镐,失蹤者是張志新(化名)和其女友劉穎反镇,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娘汞,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歹茶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了你弦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惊豺。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖禽作,靈堂內(nèi)的尸體忽然破棺而出尸昧,到底是詐尸還是另有隱情,我是刑警寧澤旷偿,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布烹俗,位于F島的核電站,受9級(jí)特大地震影響萍程,放射性物質(zhì)發(fā)生泄漏幢妄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一茫负、第九天 我趴在偏房一處隱蔽的房頂上張望蕉鸳。 院中可真熱鬧,春花似錦忍法、人聲如沸置吓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽衍锚。三九已至,卻和暖如春嗤堰,著一層夾襖步出監(jiān)牢的瞬間戴质,已是汗流浹背度宦。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留告匠,地道東北人戈抄。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像后专,于是被迫代替她去往敵國和親划鸽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • http://www.cocoachina.com/ios/20150316/11335.html iOS線程no...
    紫色冰雨閱讀 235評(píng)論 0 0
  • 1.KVO甘畅,即:Key-Value Observing埂蕊,它提供一種機(jī)制,當(dāng)指定的對(duì)象的屬性被修改后疏唾,則對(duì)象就會(huì)接受...
    BEYOND黃閱讀 1,542評(píng)論 0 6
  • 轉(zhuǎn)載自南峰子的技術(shù)博客 一個(gè)NSNotificationCenter對(duì)象(通知中心)提供了在程序中廣播消息的機(jī)制蓄氧,...
    我消失1314閱讀 890評(píng)論 0 2
  • 我只記得那一夜很冷,好像要把我凍昏過去槐脏,非常糟糕的一個(gè)傍晚喉童。其他的我都沒有印象了。 ――苗青 我好...
    糊成粥閱讀 441評(píng)論 5 1
  • 1、 曾經(jīng)一個(gè)同事問我:“親情露氮、友情祖灰、愛情這三樣,你覺得最重要的是什么畔规?” 我毫不猶豫地回答:“友情局扶。” 她不可思...
    欣然亦君閱讀 780評(píng)論 0 1