通知的使用
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];
}
程序比较简单,这里说一下使用通知的步骤:
- 在需要监听某通知的地方注册通知监听器
- 实现通知监听器的回调函数
- 在监听器对象销毁前删除通知监听器
- 如有通知需要发送,使用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,然后自定义相关的处理。