通知的使用
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];
}
程序比較簡單斗忌,這里說一下使用通知的步驟:
- 在需要監(jiān)聽某通知的地方注冊通知監(jiān)聽器
- 實現(xiàn)通知監(jiān)聽器的回調(diào)函數(shù)
- 在監(jiān)聽器對象銷毀前刪除通知監(jiān)聽器
- 如有通知需要發(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)的處理。