Notification 与多线程

独家号 李峰峰博客 作者 李峰峰 原文链接

一、概述

在多线程中,无论在哪个线程注册了观察者,Notification接收和处理都是在发送Notification的线程中的。所以,当我们需要在接收到Notification后作出更新UI操作的话,就需要考虑线程的问题了,如果在子线程中发送Notification,想要在接收到Notification后更新UI的话就要切换回到主线程。先看一个例子:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *NOTIFICATION_NAME = @"NOTIFICATION_NAME";
    
    NSLog(@"Current thread = %@", [NSThread currentThread]);
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:NOTIFICATION_NAME object:nil];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
         NSLog(@"Post notification,Current thread = %@", [NSThread currentThread]);
        
        [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:nil userInfo:nil];
       
        
    });
    
}
 
- (void)handleNotification:(NSNotification *)notification {
    NSLog(@"Receive notification,Current thread = %@", [NSThread currentThread]);
}

运行结果:

2017-03-11 17:56:33.898 NotificationTest[23457:1615587] Current thread = <NSThread: 0x608000078080>{number = 1, name = main}
2017-03-11 17:56:33.899 NotificationTest[23457:1615738] Post notification,Current thread = <NSThread: 0x60000026c500>{number = 3, name = (null)}
2017-03-11 17:56:33.899 NotificationTest[23457:1615738] Receive notification,Current thread = <NSThread: 0x60000026c500>{number = 3, name = (null)}

上面我们在主线程注册观察者,在子线程发送Notification,最后Notification的接收和处理也是在子线程。

二、重定向Notification到指定线程

当然,想要在子线程发送Notification、接收到Notification后在主线程中做后续操作,可以用一个很笨的方法,在 handleNotification 里面强制切换线程:

- (void)handleNotification:(NSNotification *)notification {
   NSLog(@"Receive notification,Current thread = %@", [NSThread currentThread]);
   dispatch_async(dispatch_get_main_queue(), ^{
      NSLog(@"Current thread = %@", [NSThread currentThread]);
   });
}

在简单情况下可以使用这种方法,但是当我们发送了多个Notification并且有多个观察者的时候,难道我们要在每个地方都手动切换线程?所以,这种方法并不是一个有效的方法。

最好的方法是在Notification所在的默认线程中捕获发送的通知,然后将其重定向到指定的线程中。关于Notification的重定向官方文档给出了一个方法:

Snip20170311_1

翻译成中文:

我们根据官方文档中的教程测试一下:

//
//  ViewController.m
//  NotificationTest
//
//  Created by 李峰峰 on 2017/3/11.
//  Copyright © 2017年 李峰峰. All rights reserved.
//
 
#import "ViewController.h"
 
@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];
    
    NSString *NOTIFICATION_NAME = @"NOTIFICATION_NAME";
 
    NSLog(@"Current thread = %@", [NSThread currentThread]);
    
    [self setUpThreadingSupport];
    
    //注册观察者
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:NOTIFICATION_NAME object:nil];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        //发送Notification
        NSLog(@"Post notification,Current thread = %@", [NSThread currentThread]);
        [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NAME object:nil userInfo:nil];
        
    });
}
 
/*
    在注册任何通知之前,需要先初始化属性。下面方法初始化了队列和锁定对象,保留对当前线程对象的引用,并创建一个Mach通信端口,将其添加到当前线程的运行循环中。
    此方法运行后,发送到notificationPort的任何消息都会在首次运行此方法的线程的run loop中接收。如果接收线程的run loop在Mach消息到达时没有运行,则内核保持该消息,直到下一次进入run loop。接收线程的run loop将传入消息发送到端口delegate的handleMachMessage:方法。
 */
- (void) setUpThreadingSupport {
    if (self.notifications) {
        return;
    }
    self.notifications      = [[NSMutableArray alloc] init];
    self.notificationLock   = [[NSLock alloc] init];
    self.notificationThread = [NSThread currentThread];
    
    self.notificationPort = [[NSMachPort alloc] init];
    [self.notificationPort setDelegate:self];
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort
                                forMode:(__bridge NSString*)kCFRunLoopCommonModes];
}
 
 
/**
 端口的代理方法
 */
- (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) {
        // 将Notification转发到目标线程
        [self.notificationLock lock];
        [self.notifications addObject:notification];
        [self.notificationLock unlock];
        [self.notificationPort sendBeforeDate:[NSDate date]
                                   components:nil
                                         from:nil
                                     reserved:0];
    }else {
        // 在此处理通知
        NSLog(@"Receive notification,Current thread = %@", [NSThread currentThread]);
        NSLog(@"Process notification");
    }
}
 
@end

打印结果:

2017-03-11 18:28:55.788 NotificationTest[24080:1665269] Current thread = <NSThread: 0x60800006d4c0>{number = 1, name = main}
2017-03-11 18:28:55.789 NotificationTest[24080:1665396] Post notification,Current thread = <NSThread: 0x60800026bc40>{number = 4, name = (null)}
2017-03-11 18:28:55.795 NotificationTest[24080:1665269] Receive notification,Current thread = <NSThread: 0x60800006d4c0>{number = 1, name = main}
2017-03-11 18:28:55.795 NotificationTest[24080:1665269] Process notification

可以看到,运行结果结果我们想要的:在子线程中发送Notification,在主线程中接收与处理Notification。

上面的实现方法也不是绝对完美的,苹果官方指出了这种方法的限制:

(1)所有线程的Notification的处理都必须通过相同的方法(processNotification :)。

(2)每个对象必须提供自己的实现和通信端口。

更好但更复杂的方法是我们自己去子类化一个NSNotificationCenter,或者单独写一个类来处理这种转发。

除了上面苹果官方给我们提供的方法外,我们还可以利用基于block的NSNotification去实现,apple 从 ios4 之后提供了带有 block 的 NSNotification。使用方式如下:

 - (id<NSObject>)addObserverForName:(NSString *)name
                            object:(id)obj
                             queue:(NSOperationQueue *)queue
                        usingBlock:(void (^)(NSNotification *note))block

其中:

  • 观察者就是当前对象
  • queue 定义了 block 执行的线程,nil 则表示 block 的执行线程和发通知在同一个线程
  • block 就是相应通知的处理函数

这个 API 已经能够让我们方便的控制通知的线程切换。但是,这里有个问题需要注意。就是其 remove 操作。

原来的 NSNotification 的 remove 方式如下:

- (void)removeObservers {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:POST_NOTIFICATION object:nil];
}

但是带 block 方式的 remove 便不能像上面这样处理了。其方式如下:

- (void)removeObservers {
    if(_observer){
        [[NSNotificationCenter defaultCenter] removeObserver:_observer];
    }
}

其中 _observer 是 addObserverForName 方式的 api 返回观察者对象。这也就意味着,你需要为每一个观察者记录一个成员对象,然后在 remove 的时候依次删除。试想一下,你如果需要 10 个观察者,则需要记录 10 个成员对象,这个想想就是很麻烦,而且它还不能够方便的指定 observer 。因此,理想的做法就是自己再做一层封装,将这些细节封装起来。

开发者头条

程序员分享平台