浅谈 iOS NSNotification

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容