NSNotification通知的使用和多线程

通知的使用

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,然后自定义相关的处理。

参考

  1. iOS多线程中使用NSNotification
  2. Notification与多线程
  3. NSNotificationCenter 通知使用方法详解
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容