通知的使用
NSNotificationCenter通知中心是iOS程序內部的一種消息廣播的實現機制,可以在不同對象之間發送通知進而實現通信,通知中心采用的是一對多的方式,一個對象發送的通知可以被多個對象接收,這一點與KVO機制類似,KVO觸發的回調函數也可以被對個對象響應,但代理模式delegate則是一對一的模式,委托對象只能有一個,對象也只能和委托對象通過代理的方式通信。
通知機制中比較核心的兩個類:NSNotification和NSNotificationCenter
NSNotification
NSNotification是通知中心的基礎,通知中心發送的通知都會被封裝成該類的對象進而在不同對象間傳遞。類定義如下:
//通知的名稱,可以根據名稱區分不同的通知
@property (readonly, copy) NSNotificationName name;
//通知的對象,常使用nil,如果設置了值的話注冊的通知監聽器的object需要與通知的object匹配,否則接收不到通知
@property (nullable, readonly, retain) id object;
//字典類型的用戶信息,用戶可將需要傳遞的數據放入該字典中
@property (nullable, readonly, copy) NSDictionary *userInfo;
//下面三個是NSNotification的構造函數,一般不需要手動構造
- (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
通知中心采用單例的模式,整個系統只有一個通知中心。可以通過[NSNotificationCenter defaultCenter]來獲取對象。
通知中心的幾個核心方法如下:
/*
注冊通知監聽器,這是唯一的注冊通知的方法
observer為監聽器
aSelector為接到收通知后的處理函數
aName為監聽的通知的名稱
object為接收通知的對象,需要與postNotification的object匹配,否則接收不到通知
*/
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
/*
發送通知,需要手動構造一個NSNotification對象
*/
- (void)postNotification:(NSNotification *)notification;
/*
發送通知
aName為注冊的通知名稱
anObject為接受通知的對象,通知不傳參時可使用該方法
*/
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
/*
發送通知
aName為注冊的通知名稱
anObject為接受通知的對象
aUserInfo為字典類型的數據,可以傳遞相關數據
*/
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
/*
刪除通知的監聽器
*/
- (void)removeObserver:(id)observer;
/*
刪除通知的監聽器
aName監聽的通知的名稱
anObject監聽的通知的發送對象
*/
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
/*
以block的方式注冊通知監聽器
*/
- (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中有一個按鈕和一個標簽,點擊按鈕跳轉到NextViewController視圖中,NextViewController中包含一個輸入框和一個按鈕,用戶在完成輸入后點擊按鈕退出視圖跳轉回ViewController并在ViewController的標簽中展示用戶填寫的數據。代碼如下
//ViewController部分代碼
- (void)viewDidLoad
{
//注冊通知的監聽器,通知名稱為inputTextValueChangedNotification,處理函數為inputTextValueChangedNotificationHandler:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputTextValueChangedNotificationHandler:) name:@"inputTextValueChangedNotification" object:nil];
}
//按鈕點擊事件處理器
- (void)buttonClicked
{
//按鈕點擊后創建NextViewController并展示
NextViewController *nvc = [[NextViewController alloc] init];
[self presentViewController:nvc animated:YES completion:nil];
}
//通知監聽器處理函數
- (void)inputTextValueChangedNotificationHandler:(NSNotification*)notification
{
//從userInfo字典中獲取數據展示到標簽中
self.label.text = notification.userInfo[@"inputText"];
}
- (void)dealloc
{
//當ViewController銷毀前刪除通知監聽器
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"inputTextValueChangedNotification" object:nil];
}
//NextViewController部分代碼
//用戶完成輸入后點擊按鈕的事件處理器
- (void)completeButtonClickedHandler
{
//發送通知,并構造一個userInfo的字典數據類型,將用戶輸入文本保存
[[NSNotificationCenter defaultCenter] postNotificationName:@"inputTextValueChangedNotification" object:nil userInfo:@{@"inputText": self.textField.text}];
//退出視圖
[self dismissViewControllerAnimated:YES completion:nil];
}
程序比較簡單,這里說一下使用通知的步驟:
1、在需要監聽某通知的地方注冊通知監聽器
2、實現通知監聽器的回調函數
3、在監聽器對象銷毀前刪除通知監聽器
4、如有通知需要發送,使用NSNotificationCenter的postNotification方法發送通知
在iOS9以后蘋果開始不再對已經銷毀的監聽器發送通知,當監聽器對象銷毀后發送通知也不會造成野指針錯誤,這一點比KVO更加安全,KVO在監聽器對象銷毀后仍會觸發回調函數就可能造成野指針錯誤,因此使用通知也就可以不手動刪除監聽器了,但如果需要適配iOS9之前的系統還是需要養成手動刪除監聽器的習慣。
通知中的多線程
在蘋果官方文檔中,對于多線程中使用通知有如下解釋:
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,就在哪個線程中被轉發,而不一定是在注冊觀察者的那個線程中。
也就是說Notification的發送與接收處理都是在同一個線程中。
可以用下面代碼驗證:
- (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(@"發送通知的線程為%@", [NSThread currentThread]);
});
}
- (void)handleNotification: (NSNotification *)notification {
NSLog(@"轉發通知的線程%@", [NSThread currentThread]);
}
輸出結果為:
當前線程為<NSThread: 0x608000073780>{number = 1, name = main}
接收和處理通知的線程<NSThread: 0x608000261180>{number = 3, name = (null)}
發送通知的線程為<NSThread: 0x608000261180>{number = 3, name = (null)}
可以看到,雖然我們在主線程中注冊了通知的觀察者,但在全局隊列中post的Notification,并不是在主線程處理的。所以,這時候就需要注意,如果我們想在回調中處理與UI相關的操作,需要確保是在主線程中執行回調。
那么怎么才能做到一個Notification的post線程與轉發線程不是同一個線程呢?蘋果文檔給了一種解決方法:
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所在的默認線程中捕獲這些分發的通知,然后將其重定向到指定的線程中。
方式一:利用block
從 iOS4 之后蘋果提供了帶有 block 的 NSNotification。使用方式如下:
-(id)addObserverForName:(NSString*)name object:(id)obj queue:(NSOperationQueue*)queue usingBlock:^(NSNotification * _Nonnull note);
我們在使用該block方法時,只要設置[NSOperationQueuemainQueue],就可以實現在主線程中刷新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(@"發送通知的線程為%@", [NSThread currentThread]);
});
方式二:自定義通知隊列
自定義一個通知隊列(注意,不是NSNotificationQueue對象,而是一個數組),讓這個隊列去維護那些我們需要重定向的Notification。我們仍然是像平常一樣去注冊一個通知的觀察者,當Notification來了時,先看看post這個Notification的線程是不是我們所期望的線程,如果不是,則將這個Notification存儲到我們的隊列中,并發送一個信號(signal)到期望的線程中,來告訴這個線程需要處理一個Notification。指定的線程在收到信號后,將Notification從隊列中移除,并進行處理。
這種方式蘋果官方提供了代碼示例,如下:
@interface ViewController () <NSMachPortDelegate>
@property (nonatomic) NSMutableArray *notifications; // 通知隊列
@property (nonatomic) NSThread *notificationThread; // 期望線程
@property (nonatomic) NSLock *notificationLock; // 用于對通知隊列加鎖的鎖對象,避免線程沖突
@property (nonatomic) NSMachPort *notificationPort; // 用于向期望線程發送信號的通信端口
@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沒有運行時,則內核會保存這條消息,直到下一次進入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,如愿地在主線程中接收到了。然而這種方式存在缺陷,正如蘋果官網所說:
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.
更好的實現方式是我們去子例化一個NSNotificationCenter,然后自定義相關的處理。
作者:Gintok
鏈接:http://www.lxweimin.com/p/e368a18ca7c2
來源:簡書