NSNotification通知的使用和多線程

通知的使用

NSNotificationCenter通知中心是iOS程序內(nèi)部的一種消息廣播的實現(xiàn)機制,可以在不同對象之間發(fā)送通知進而實現(xiàn)通信匈睁,通知中心采用的是一對多的方式,一個對象發(fā)送的通知可以被多個對象接收,這一點與KVO機制類似,KVO觸發(fā)的回調(diào)函數(shù)也可以被對個對象響應脾拆,但代理模式delegate則是一對一的模式馒索,委托對象只能有一個,對象也只能和委托對象通過代理的方式通信名船。

通知機制中比較核心的兩個類:NSNotification和NSNotificationCenter

NSNotification

NSNotification是通知中心的基礎(chǔ)双揪,通知中心發(fā)送的通知都會被封裝成該類的對象進而在不同對象間傳遞。類定義如下:

//通知的名稱包帚,可以根據(jù)名稱區(qū)分不同的通知
@property (readonly, copy) NSNotificationName name;
//通知的對象,常使用nil运吓,如果設(shè)置了值的話注冊的通知監(jiān)聽器的object需要與通知的object匹配渴邦,否則接收不到通知
@property (nullable, readonly, retain) id object;
//字典類型的用戶信息,用戶可將需要傳遞的數(shù)據(jù)放入該字典中
@property (nullable, readonly, copy) NSDictionary *userInfo;

//下面三個是NSNotification的構(gòu)造函數(shù)拘哨,一般不需要手動構(gòu)造
- (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)) NS_DESIGNATED_INITIALIZER;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject;
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

NSNotificationCenter

通知中心采用單例的模式谋梭,整個系統(tǒng)只有一個通知中心【肭啵可以通過[NSNotificationCenter defaultCenter]來獲取對象瓮床。
通知中心的幾個核心方法如下:

/*
注冊通知監(jiān)聽器,這是唯一的注冊通知的方法
observer為監(jiān)聽器
aSelector為接到收通知后的處理函數(shù)
aName為監(jiān)聽的通知的名稱
object為接收通知的對象产镐,需要與postNotification的object匹配隘庄,否則接收不到通知
*/
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;

/*
發(fā)送通知,需要手動構(gòu)造一個NSNotification對象
*/
- (void)postNotification:(NSNotification *)notification;

/*
發(fā)送通知
aName為注冊的通知名稱
anObject為接受通知的對象癣亚,通知不傳參時可使用該方法
*/
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;

/*
發(fā)送通知
aName為注冊的通知名稱
anObject為接受通知的對象
aUserInfo為字典類型的數(shù)據(jù)丑掺,可以傳遞相關(guān)數(shù)據(jù)
*/
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

/*
刪除通知的監(jiān)聽器
*/
- (void)removeObserver:(id)observer;

/*
刪除通知的監(jiān)聽器
aName監(jiān)聽的通知的名稱
anObject監(jiān)聽的通知的發(fā)送對象
*/
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

/*
以block的方式注冊通知監(jiān)聽器
*/
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

我們來看看實際使用通知的例子,有兩個頁面述雾,ViewController和NextViewController街州,在ViewController中有一個按鈕和一個標簽,點擊按鈕跳轉(zhuǎn)到NextViewController視圖中玻孟,NextViewController中包含一個輸入框和一個按鈕唆缴,用戶在完成輸入后點擊按鈕退出視圖跳轉(zhuǎn)回ViewController并在ViewController的標簽中展示用戶填寫的數(shù)據(jù)。代碼如下

//ViewController部分代碼

- (void)viewDidLoad
{
    //注冊通知的監(jiān)聽器黍翎,通知名稱為inputTextValueChangedNotification面徽,處理函數(shù)為inputTextValueChangedNotificationHandler:
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputTextValueChangedNotificationHandler:) name:@"inputTextValueChangedNotification" object:nil];

}

//按鈕點擊事件處理器
- (void)buttonClicked
{
    //按鈕點擊后創(chuàng)建NextViewController并展示
    NextViewController *nvc = [[NextViewController alloc] init];
    [self presentViewController:nvc animated:YES completion:nil];
}

//通知監(jiān)聽器處理函數(shù)
- (void)inputTextValueChangedNotificationHandler:(NSNotification*)notification
{
    //從userInfo字典中獲取數(shù)據(jù)展示到標簽中
    self.label.text = notification.userInfo[@"inputText"];
}

- (void)dealloc
{
    //當ViewController銷毀前刪除通知監(jiān)聽器
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"inputTextValueChangedNotification" object:nil];
}

//NextViewController部分代碼
//用戶完成輸入后點擊按鈕的事件處理器
- (void)completeButtonClickedHandler
{
    //發(fā)送通知,并構(gòu)造一個userInfo的字典數(shù)據(jù)類型匣掸,將用戶輸入文本保存
    [[NSNotificationCenter defaultCenter] postNotificationName:@"inputTextValueChangedNotification" object:nil userInfo:@{@"inputText": self.textField.text}];
    //退出視圖
    [self dismissViewControllerAnimated:YES completion:nil];
}

程序比較簡單斗忌,這里說一下使用通知的步驟:

  1. 在需要監(jiān)聽某通知的地方注冊通知監(jiān)聽器
  2. 實現(xiàn)通知監(jiān)聽器的回調(diào)函數(shù)
  3. 在監(jiān)聽器對象銷毀前刪除通知監(jiān)聽器
  4. 如有通知需要發(fā)送,使用NSNotificationCenter的postNotification方法發(fā)送通知

在iOS9以后蘋果開始不再對已經(jīng)銷毀的監(jiān)聽器發(fā)送通知旺聚,當監(jiān)聽器對象銷毀后發(fā)送通知也不會造成野指針錯誤织阳,這一點比KVO更加安全,KVO在監(jiān)聽器對象銷毀后仍會觸發(fā)回調(diào)函數(shù)就可能造成野指針錯誤砰粹,因此使用通知也就可以不手動刪除監(jiān)聽器了唧躲,但如果需要適配iOS9之前的系統(tǒng)還是需要養(yǎng)成手動刪除監(jiān)聽器的習慣造挽。

通知中的多線程

在蘋果官方文檔中,對于多線程中使用通知有如下解釋:

Regular notification centers deliver notifications on the thread in which the notification was posted. Distributed notification centers deliver notifications on the main thread. At times, you may require notifications to be delivered on a particular thread that is determined by you instead of the notification center. 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.

簡單理解就是

在多線程應用中弄痹,Notification在哪個線程中post饭入,就在哪個線程中被轉(zhuǎn)發(fā),而不一定是在注冊觀察者的那個線程中肛真。

也就是說Notification的發(fā)送與接收處理都是在同一個線程中谐丢。可以用下面代碼驗證:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"當前線程為%@", [NSThread currentThread]);
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"Test_Notification" object:nil];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"Test_Notification" object:nil userInfo:nil];
        NSLog(@"發(fā)送通知的線程為%@", [NSThread currentThread]);
    });
}

- (void)handleNotification: (NSNotification *)notification {
    NSLog(@"轉(zhuǎn)發(fā)通知的線程%@", [NSThread currentThread]);
}

輸出結(jié)果為:

當前線程為<NSThread: 0x608000073780>{number = 1, name = main}
接收和處理通知的線程<NSThread: 0x608000261180>{number = 3, name = (null)}
發(fā)送通知的線程為<NSThread: 0x608000261180>{number = 3, name = (null)}

可以看到蚓让,雖然我們在主線程中注冊了通知的觀察者乾忱,但在全局隊列中post的Notification,并不是在主線程處理的历极。所以窄瘟,這時候就需要注意,如果我們想在回調(diào)中處理與UI相關(guān)的操作趟卸,需要確保是在主線程中執(zhí)行回調(diào)蹄葱。
那么怎么才能做到一個Notification的post線程與轉(zhuǎn)發(fā)線程不是同一個線程呢?蘋果文檔給了一種解決方法:

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.

這里講到了“重定向”锄列,就是我們在Notification所在的默認線程中捕獲這些分發(fā)的通知图云,然后將其重定向到指定的線程中。

方式一:利用block

從 iOS4 之后蘋果提供了帶有 block 的 NSNotification邻邮。使用方式如下:

-(id)addObserverForName:(NSString*)name object:(id)obj queue:(NSOperationQueue*)queue usingBlock:^(NSNotification * _Nonnull note);

我們在使用該block方法時琼稻,只要設(shè)置[NSOperationQueuemainQueue],就可以實現(xiàn)在主線程中刷新UI的操作饶囚。
我們的代碼也因此變得簡潔了一些:

[[NSNotificationCenter defaultCenter] addObserverForName:@"Test_Notification" object:nil queue [NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"接收和處理通知的線程%@", [NSThread currentThread]);
    }];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"Test_Notification" object:nil userInfo:nil];
    NSLog(@"發(fā)送通知的線程為%@", [NSThread currentThread]);
});

方式二:自定義通知隊列

自定義一個通知隊列(注意帕翻,不是NSNotificationQueue對象,而是一個數(shù)組)萝风,讓這個隊列去維護那些我們需要重定向的Notification嘀掸。我們?nèi)匀皇窍衿匠R粯尤プ砸粋€通知的觀察者,當Notification來了時规惰,先看看post這個Notification的線程是不是我們所期望的線程睬塌,如果不是,則將這個Notification存儲到我們的隊列中歇万,并發(fā)送一個信號(signal)到期望的線程中揩晴,來告訴這個線程需要處理一個Notification。指定的線程在收到信號后贪磺,將Notification從隊列中移除硫兰,并進行處理。
這種方式蘋果官方提供了代碼示例寒锚,如下:

@interface ViewController () <NSMachPortDelegate>
@property (nonatomic) NSMutableArray    *notifications;         // 通知隊列
@property (nonatomic) NSThread          *notificationThread;    // 期望線程
@property (nonatomic) NSLock            *notificationLock;      // 用于對通知隊列加鎖的鎖對象劫映,避免線程沖突
@property (nonatomic) NSMachPort        *notificationPort;      // 用于向期望線程發(fā)送信號的通信端口
    
@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;
    
    // 往當前線程的run loop添加端口源
    // 當Mach消息到達而接收線程的run loop沒有運行時违孝,則內(nèi)核會保存這條消息,直到下一次進入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

可以看到泳赋,我們在全局dispatch隊列中拋出的Notification雌桑,如愿地在主線程中接收到了。然而這種方式存在缺陷祖今,正如蘋果官網(wǎng)所說:

This implementation is limited in several aspects. First, all threaded notifications processed by this object must pass through the same method (processNotification:). Second, each object must provide its own implementation and communication port. A better, but more complex, implementation would generalize the behavior into either a subclass of NSNotificationCenter or a separate class that would have one notification queue for each thread and be able to deliver notifications to multiple observer objects and methods.

更好的實現(xiàn)方式是我們?nèi)プ永粋€NSNotificationCenter校坑,然后自定義相關(guān)的處理。

參考

  1. iOS多線程中使用NSNotification
  2. Notification與多線程
  3. NSNotificationCenter 通知使用方法詳解
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末千诬,一起剝皮案震驚了整個濱河市耍目,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌大渤,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掸绞,死亡現(xiàn)場離奇詭異泵三,居然都是意外死亡,警方通過查閱死者的電腦和手機衔掸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門烫幕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人敞映,你說我怎么就攤上這事较曼。” “怎么了振愿?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵捷犹,是天一觀的道長。 經(jīng)常有香客問我冕末,道長萍歉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任档桃,我火速辦了婚禮枪孩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘藻肄。我一直安慰自己蔑舞,他們只是感情好,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布嘹屯。 她就那樣靜靜地躺著攻询,像睡著了一般。 火紅的嫁衣襯著肌膚如雪州弟。 梳的紋絲不亂的頭發(fā)上蜕窿,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天谋逻,我揣著相機與錄音,去河邊找鬼桐经。 笑死毁兆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的阴挣。 我是一名探鬼主播气堕,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼畔咧!你這毒婦竟也來了茎芭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤誓沸,失蹤者是張志新(化名)和其女友劉穎梅桩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拜隧,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡宿百,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了洪添。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垦页。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖干奢,靈堂內(nèi)的尸體忽然破棺而出痊焊,到底是詐尸還是另有隱情,我是刑警寧澤忿峻,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布薄啥,位于F島的核電站,受9級特大地震影響逛尚,放射性物質(zhì)發(fā)生泄漏罪佳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一黑低、第九天 我趴在偏房一處隱蔽的房頂上張望赘艳。 院中可真熱鬧,春花似錦克握、人聲如沸蕾管。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掰曾。三九已至,卻和暖如春停团,著一層夾襖步出監(jiān)牢的瞬間旷坦,已是汗流浹背掏熬。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留秒梅,地道東北人旗芬。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像捆蜀,于是被迫代替她去往敵國和親疮丛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

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

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,321評論 8 265
  • 近期接觸項目中用的通知比較多辆它,對于通知有一個系統(tǒng)的理解與學習誊薄,已下是做的一些總結(jié) 先來看看官方的文檔,是這樣寫的:...
    9de75b652cd9閱讀 586評論 0 0
  • http://www.cocoachina.com/ios/20150316/11335.html iOS線程no...
    紫色冰雨閱讀 232評論 0 0
  • 莘竹閱讀 155評論 0 2
  • 光陰綠了窗紗锰茉,黃了梨箋呢蔫,散了繁花。 往日廳堂飒筑,絲描屋角片吊,望斷天涯。 尋夢能不憶它扬霜?舊山誰采新茶定鸟? 一地殘紅而涉,一世嗟...
    綠楊蔭閱讀 260評論 6 14