浅谈 iOS NSNotification

作者: s_在路上 | 来源:发表于2018-09-13 17:39 被阅读17次

    NSNotification 解决的问题

    • 可以实现跨层的传递,例如A页面跳转到B页面,B页面再跳转到C页面,这时候如果我们通过委托回调的模式让A知道C的一些修改,那么实现起来就会很麻烦。
    • 可以实现一对多,NSNotification 的实际是一种观察者模式。

    NSNotificationCenter

    NSNotificationCenter 就相当于一个广播站,使用 [NSNotificationCenter defaultCenter] 来获取,NSNotificationCenter 实际上是 iOS 程序内部之间的一种消息广播机制,主要为了解决应用程序内部不同对象之间解耦而设计。
    NSNotificationCenter 是整个通知机制的关键所在,它管理着监听者的注册和注销,通知的发送和接收。NSNotificationCenter 维护着一个通知的分发表,把所有通知发送者发送的通知,转发给对应的监听者们。每一个 iOS 程序都有一个唯一的通知中心,不必自己去创建一个,它是一个单例,通过 [NSNotificationCenter defaultCenter] 方法获取。
    NSNotificationCenter 是基于观察者模式设计的,不能跨应用程序进程通信,当 NSNotificationCenter 接收到消息之后会根据内部的消息转发表,将消息发送给订阅者;它可以向应用任何地方发送和接收通知。
    NSNotificationCenter 注册观察者,发送者使用通知中心广播时,以 NSNotificationnameobject 来确定需要发送给哪个观察者。为保证观察者能接收到通知,所以应先向通知中心注册观察者,接着再发送通知这样才能在通知中心调度表中查找到相应观察者进行通知。

    NSNotification

    NSNotificationNSNotificationCenter 接收到消息之后根据内部的消息转发表,将消息发送给订阅者封装的对象;

    @interface NSNotification : NSObject <NSCopying, NSCoding>
    
    //这个成员变量是这个消息对象的唯一标识,用于辨别消息对象
    @property (readonly, copy) NSString *name;
    // 这个成员变量定义一个对象,可以理解为针对某一个对象的消息,代表通知的发送者
    @property (nullable, readonly, retain) id object;
    //这个成员变量是一个字典,可以用其来进行传值
    @property (nullable, readonly, copy) NSDictionary *userInfo;
    // 初始化方法
    - (instancetype)initWithName:(NSString *)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo NS_AVAILABLE(10_6, 4_0) NS_DESIGNATED_INITIALIZER;
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
    
    @end
    

    由于 NSNotification 属性都是只读的,如果要创建通知则要用下面 NSNotification(NSNotificationCreation) 分类相应的方法进行初始化;

    NSNotification 不能通过 init 实例化,这样会引起下面的异常,比如:

    NSNotification *notif = [[NSNotification alloc] init];
    
    *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** -[NSConcreteNotification init]: should never be used'
    
    @interface NSNotification (NSNotificationCreation)
    + (instancetype)notificationWithName:(NSString *)aName object:(nullable id)anObject;
    + (instancetype)notificationWithName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
    - (instancetype)init /*NS_UNAVAILABLE*/;    /* do not invoke; not a valid initializer for this class */
    @end
    

    注意:
    如果 NSNotification 对象中的 notificationNamenil,则会接收所有的通知。通知中心是以 NSNotificationnameobject 来确定需要发送给哪个观察者。监听同一条通知的多个观察者,在通知到达时,它们执行回调的顺序是不确定的,所以我们不能去假设操作的执行会按照添加观察者的顺序来执行。

    通知中心默认是以同步的方式发送通知的,也就是说,当一个对象发送了一个通知,只有当该通知的所有接受者都接受到了通知中心分发的通知消息并且处理完成后,发送通知的对象才能继续执行接下来的方法。

    NSNotificationQueue

    NSNotificationQueue 通知队列,用来管理多个通知的调用。通知队列通常以先进先出 FIFO 顺序维护通。NSNotificationQueue 就像一个缓冲池把一个个通知放进池子中,使用特定方式通过 NSNotificationCenter 发送到相应的观察者。下面我们会提到特定的方式即合并通知和异步通知。

    创建通知队列方法:

    - (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;
    

    往队列加入通知方法:

    - (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
    - (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;
    

    移除队列中的通知方法:

    - (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
    
    发送方式

    NSPostingStyle包括三种类型:

    typedef NS_ENUM(NSUInteger, NSPostingStyle) {
        NSPostWhenIdle = 1,
        NSPostASAP = 2,
        NSPostNow = 3  
    };
    

    NSPostWhenIdle:空闲发送通知,当运行循环处于等待或空闲状态时,发送通知,对于不重要的通知可以使用。
    NSPostASAP:尽快发送通知,当前运行循环迭代完成时,通知将会被发送,有点类似没有延迟的定时器。
    NSPostNow :同步发送通知,如果不使用合并通知和 postNotification: 一样是同步通知。

    合并通知

    NSNotificationCoalescing也包括三种类型:

    typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
        NSNotificationNoCoalescing = 0,
        NSNotificationCoalescingOnName = 1,
        NSNotificationCoalescingOnSender = 2
    };
    

    NSNotificationNoCoalescing:不合并通知。
    NSNotificationCoalescingOnName:合并相同名称的通知。
    NSNotificationCoalescingOnSender:合并相同通知和同一对象的通知。

    通过合并我们可以用来保证相同的通知只被发送一次。forModes:(nullable NSArray<NSRunLoopMode> *)modes 可以使用不同的 NSRunLoopMode 配合来发送通知,可以看出实际上 NSNotificationQueueRunLoop 的机制以及运行循环有关系,通过 NSNotificationQueue 队列来发送的通知和关联的 RunLoop 运行机制来进行的。

    iOS 9 NSNotificationCenter 无需手动移除观察者

    众所周知,在观察者对象释放之前,需要调用 removeObserver 方法,将观察者从通知中心移除,否则程序可能会出现崩溃。其实,从 iOS 9 开始,即使不移除观察者对象,程序也不会出现异常。这是为什么呢?我们先了解一下,为什么 iOS 9 之前需要手动移除观察者对象。

    MRC 时代,观察者注册时,通知中心并不会对观察者对象做 retain 操作,而是对观察者对象进行 unsafe_unretained 引用。

    // for attribute
    @property (unsafe_unretained) NSObject *unsafeProperty;
    // for variables
    NSObject *__unsafe_unretained unsafeReference;
    
    

    不安全引用(unsafe reference)和弱引用 (weak reference) 类似,它并不会让被引用的对象保持存活,但是和弱引用不同的是,当被引用的对象释放的时,不安全引用并不会自动被置为 nil,这就意味着它变成了野指针,而对野指针发送消息会导致程序崩溃。

    If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.

    而在 iOS 9 以后,通知中心持有的是注册者的 weak 指针,这时即使不对通知进行手动移除,指针也会在注册者被回收后自动置空。但是,通过 -[NSNotificationCenter addObserverForName:object:queue:usingBlock] 方法注册的观察者依然需要手动的释放,因为通知中心对它们持有的是强引用。

    NSNotification在多线程中使用

    在多线程中,无论在哪个线程注册了观察者,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 的接收和处理也是在子线程。

    注意:
    在一个多线程的程序中,发送方发送通知的线程通常就是监听者接受通知的线程,这可能和监听者注册时的线程不一样。

    解决方法
    MachPort的使用方式

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

    一种重定向的实现思路是自定义一个通知队列(不是 NSNotificationQueue 对象),让这个队列去维护那些我们需要重定向的 Notification。我们仍然是像之前一样去注册一个通知的观察者,当 Notification 到达时,先看看 post 这个 Notification 的线程是不是我们所期望的线程,如果不是,就将这个 Notification 放到我们的队列中,然后发送一个信号signal到期望的线程中,来告诉这个线程需要处理一个 Notification。指定的线程收到这个信号signal后,将 Notification 从队列中移除,并进行后续处理。

    //  ViewController.m
    //  NotificationTest
    //
    //  Created by sunjinshuai on 2017/3/11.
    //  Copyright © 2017年 sunjinshuai. 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
    

    在发送通知的子线程处理通知的事件时,将 NSNotification 暂存,然后通过 MachPort 往相应线程的 RunLoop 中发送事件。相应的线程收到该事件后,取出在队列中暂存的 NSNotification , 然后在当前线程中调用处理通知的方法。
    可以看到,运行结果结果我们想要的:在子线程中发送 Notification,在主线程中接收与处理 Notification

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

    • 所有线程的 Notification 的处理都必须通过相同的方法 processNotification:
    • 每个对象必须提供自己的实现和通信端口。
    block

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

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

    其中:

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

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

    原来的 NSNotificationremove 方式如下:

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

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

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

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

    当然,想要在子线程发送 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 并且有多个观察者的时候,难道我们要在每个地方都手动切换线程?所以,这种方法并不是一个有效的方法。

    通知的实现原理

    以下源码来自于libs-base

    typedef struct NCTbl {
      Observation       *wildcard;  /* Get ALL messages.        */
      GSIMapTable       nameless;   /* Get messages for any name.   */
      GSIMapTable       named;      /* Getting named messages only. */
      unsigned      lockCount;  /* Count recursive operations.  */
      NSRecursiveLock   *_lock;     /* Lock out other threads.  */
      Observation       *freeList;
      Observation       **chunks;
      unsigned      numChunks;
      GSIMapTable       cache[CACHESIZE];
      unsigned short    chunkIndex;
      unsigned short    cacheIndex;
    } NCTable;
    
    typedef struct  Obs {
      id        observer;   /* Object to receive message.   */
      SEL       selector;   /* Method selector.     */
      struct Obs    *next;      /* Next item in linked list.    */
      int       retained;   /* Retain count for structure.  */
      struct NCTbl  *link;      /* Pointer back to chunk table  */
    } Observation;
    

    从源码中可以看出,在 NSNotificationCenter 内部一共保存了两张 MapTable。一张用于保存添加观察者的时候传入了 NotifcationName 的情况;一张用于保存添加观察者的时候没有传入了 NotifcationName 的情况。

    Example

    https://github.com/iOS-Advanced/iOS-Advanced/tree/master/sourcecode/NSNotificationTheory

    相关文章

      网友评论

        本文标题:浅谈 iOS NSNotification

        本文链接:https://www.haomeiwen.com/subject/tuawgftx.html