iOS中的NSNotification

NSNotification概念

通知(NSNotification)是在程序中实现协调和内聚的强大机制,它减少了对程序中对象之间的强依赖关系的需要(这种依赖关系会降低这些对象的可重用性)。

通知机制的核心是一个与线程关联的单例对象,也就是通知中心(NSNotifacationCenter)。通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。


image.png

NSNotification

NSNotification包含了如下必要的字段,并且都是只读的:

@property (readonly, copy) NSNotificationName name; // 通知名称,通知的唯一标识 
@property (nullable, readonly, retain) id object; // 任意对象,通常是通知发送者 
@property (nullable, readonly, copy) NSDictionary *userInfo; // 通知的附加信息

也提供了相应的初始化方法:

- (instancetype)initWithName:(NSNotificationName)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo NS_DESIGNATED_INITIALIZER;

+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject; 
+ (instancetype)notificationWithName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo; 
- (instancetype)init /*NS_UNAVAILABLE*/; /* do not invoke; not a valid initializer for this class */

NSNotification对象是不可变的
NSNotification不能通过init初始化,否则会引起如下崩溃:

image.png

但是一般情况下不会直接这样创建通知对象。实际开发中更多的是直接调用NSNoficationCenterpostNotificationName:object:postNotificationName:object:userInfo:方法发送通知,这两个方法内部会根据传入的参数直接创建通知对象

NSNotificationCenter

NSNotificationCenter提供了一种互不相干的对象之间能够相互通信的方式。它接收NSNotification对象并把通知广播给所有感兴趣的对象。
NSNotificationCenter暴露给外部的属性只有一个defaultCenter,而且这个属性还是只读的。

@property (class, readonly, strong) NSNotificationCenter *defaultCenter;

暴露给外部的方法分为三类:添加通知观察者的方法、发出通知的方法、移除通知观察者的方法。

// 添加通知观察者 
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject; 
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block; 

// 发出通知 
- (void)postNotification:(NSNotification *)notification; - (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject; 
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo; 

// 移除通知观察者 
- (void)removeObserver:(id)observer; - (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

注意:

  • 若notificationName为nil,通知中心会通知所有与该通知中object相匹配的监听对象
  • 若anObject为nil,通知中心会通知所有与该通知中notificationName相匹配的监听对象
  • iOS9以后NSNotificationCenter无需手动移除观察者
    在观察者对象释放之前,需要调用==removeOberver==方法将观察者从通知中心移除,否则程序可能会出现崩溃。但从iOS9开始,即使不移除观察者对象,程序也不会出现异常。
    这是因为在iOS9以后,通知中心持有的观察者由==unsafe_unretained==引用变为==weak==引用。即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空。但是通过addObserverForName:object: queue:usingBlock:方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。

我们可以看下这两个方法,有什么不同:

- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject; 

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

我们看到上面两个方法中,第二个方法没有传入observer,但是返回值是一个observer,所以猜测方法内部会生成一个observer,这个observer是内部生成的,通知中心会持有它,并且block也是通过copy操作后被observer持有,所以,释放是需要我们自己处理的。
下面是GNU中的代码,它会根据队列 block生成一个观察者:

- (id) addObserverForName: (NSString *)name 
                   object: (id)object 
                    queue: (NSOperationQueue *)queue 
               usingBlock: (GSNotificationBlock)block
{
    GSNotificationObserver *observer = 
        [[GSNotificationObserver alloc] initWithQueue: queue block: block];

    [self addObserver: observer 
             selector: @selector(didReceiveNotification:) 
                 name: name 
               object: object];

    return observer;
}

当观察者收到通知的时候:

- (void) didReceiveNotification: (NSNotification *)notif
{
    if (_queue != nil)
    {
        GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc] 
            initWithNotification: notif block: _block];

        [_queue addOperation: op];
    }
    else
    {
        CALL_BLOCK(_block, notif);
    }
}

从上面可以看出来,如果我们传的是自定义队列,就会异步并发执行,如果传nil,就在当前线程中同步执行,就是一个回调了。

再看第一个方法,传入的observer是self,一般都是controller,iOS9之前通知中心和controller是一种 unsafe_unretained 关系,当controller释放之后,指向controller的unsafe_unretained指针不会被释放,但是iOS9之后使用了weak,当controller释放的时候,指向controller的weak指针置为nil,所以采用第一个方法,不需要我们手动的removeObserver。

除了在不想用的时候移除observer外,可以按照下面这样写,只调用一次,就移除:

NSNotificationCenter * __weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:@"OneTimeNotification"
                                       object:nil
                                        queue:[NSOperationQueue mainQueue]
                                   usingBlock:^(NSNotification *note) {
                                       NSLog(@"Received the notification!");
                                       [center removeObserver:token];
                                   }];

NSNotificationQueue

NSNotificationQueue通知队列充当通知中心的缓冲区。通知队列通常以FIFO的顺序来维护通知。每个线程都有一个与默认的通知中心相关的默认通知队列(defaultQueue)

// 缺省的通知队列
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;
// 指定初始化函数 - (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;

通过defaultQueue获取默认的通知队列或者通过指定初始化函数initWithNotificationCenter:创建通知队列,最终都是通过NSNotificationCenter来发送、注册通知。

// 往通知队列添加通知 
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle; 
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes; 
// 如果modes为nil,则对于runloop的所有模式发送通知都是有效的 
// 移除通知队列中的通知 
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;

合并通知:
NSNotificationNoCoalescing. 不合并队列中的通知 NSNotificationCoalescingOnName. 按通知名称合并通知 NSNotificationCoalescingOnSender. 按传入的object合并通知

发送方式
NSPostASAP. 在当前通知调用结束或计时器超时发送通知
NSPostWhenIdle. 当runloop处于空闲状态时发送通知
NSPostNow. 在合并通知后立即发送通知

其内部会持有一个通知中心,在判断发送方式NSPostingStyle的时候,如果是NSPostNow就会立即发送通知,NSPostWhenIdle会注册runloop状态的通知吧,或者说runloop状态变更的话,会调用NSNotificationQueue的方法。

下面是合并通知的例子:

 NSNotification *notification = [NSNotification notificationWithName:@"Test的通知" object:nil];
    NSNotification *notification2 = [NSNotification notificationWithName:@"Test的通知" object:nil];
    NSNotification *notification3 = [NSNotification notificationWithName:@"Test的通知" object:nil];

    [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:@[NSRunLoopCommonModes]];
    [[NSNotificationQueue defaultQueue] enqueueNotification:notification2 postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:@[NSRunLoopCommonModes]];
    [[NSNotificationQueue defaultQueue] enqueueNotification:notification3 postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:@[NSRunLoopCommonModes]];

NSNotification在多线程中的使用

无论在哪个线程中注册了观察者,通知的发送和接收都是在同一个线程中。所以当接收到通知做UI操作的时候就需要考虑线程的问题。如果在子线程中接收到通知,需要切换到主线程再做更新UI的操作。

- (void)viewDidLoad {
    [super viewDidLoad];
    static NSString *const notificationName  = @"notificationName";
    NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
    NSLog(@"主线程中注册通知");
    [defaultCenter addObserver:self selector:@selector(handleNotification:) name:notificationName object:nil];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"子线程中发送通知:%@",[NSThread currentThread]);
        [defaultCenter postNotificationName:notificationName object:nil];
    });
}

- (void)handleNotification:(NSNotification *)notify{
    NSLog(@"收到通知:%@",[NSThread currentThread]);
}

在主线程中注册了观察者,然后在子线程发送通知,最后接收和处理通知也是在子线程。一般情况下,发送通知所在的线程就是接收通知所在的线程。

将通知重定向到指定线程

解决方法:捕获发送通知所在线程的通知,然后将其重定向至指定线程。关于通知重定向,官方文档给出了一种解决方案:

一种重定向通知的方式是自定义通知队列(不是NSNotificationQueue对象)

  • 让自定义队列去维护需要重定向的通知。
  • 仍然像之前一样注册通知的观察者,
  • 当接收到通知时,判断当前线程是否是我们期望的线程,如果不是,就将通知放到自定义队列中,
  • 然后发送一个信号sigal到期望的线程中,告知这个线程需要处理通知。
  • 指定线程收到通知后,从自定义队列中把这个通知移除,并进行后续处理

代码如下:

@interface ViewController () <NSMachPortDelegate>

@property (nonatomic, strong) NSMutableArray *notifications;
@property (nonatomic, strong) NSThread *notificationThread;
@property (nonatomic, strong) NSMachPort *notificationPort;
@property (nonatomic, strong) NSLock *notificationLock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 用来存储通知
    self.notifications = [NSMutableArray array];
    // 处理通知的线程
    self.notificationThread = [NSThread currentThread];
    // 初始化锁对象
    self.notificationLock = [[NSLock alloc] init];
    // port
    self.notificationPort = [[NSMachPort alloc] init];
    self.notificationPort.delegate = self;
    [[NSRunLoop currentRunLoop] addPort:self.notificationPort forMode:NSRunLoopCommonModes];
    
    // 注册通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"testNotificationName" object:nil];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"testNotificationName" object:nil];
    });
}

- (void)handleNotification:(NSNotification *)notify{
    if ([NSThread currentThread] != self.notificationThread) {
        [self.notificationLock lock];
        [self.notifications addObject:notify];
        [self.notificationLock unlock];
        // port发送消息
        [self.notificationPort sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];
    }else{
        NSLog(@"Receive Notification. Current thread = %@", [NSThread currentThread]);
    }
}

// mach代理方法
- (void)handleMachMessage:(void *)msg{
    [self.notificationLock lock];
    while (self.notifications.count) {
        NSNotification *notify = [self.notifications firstObject];
        [self.notifications removeObjectAtIndex:0];
        [self handleNotification:notify];
    }
    [self.notificationLock unlock];
}

@end

收到消息的线程,就是主线程了。
可以使用NSMachPort处理子线程中,通知中心发送通知的通知重定向到主线程中。

通知的实现原理

NSNotification是一个类簇(这里不做介绍了),当我们调用initWithName:Object:userInfo:方法的时候,系统内部会实例化NSNotification的子类NSZConcreteNotification,在这个子类中重写了NSNotification定义的相关字段和方法。

NSNotificationCenter是通知的管理类,实现比较复杂,主要定义了两个table,同时也定义了Observation保存观察者的信息,它们两个结构体简化如下:

typedef struct NCTbl { 
Observation *wildcard; /* Get ALL messages. */ 
GSIMapTable nameless; /* Get messages for any name. */ 
GSIMapTable named; /* Getting named messages only. */ 
} 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内部保存了两张表:

  1. GSIMapTable named一张用于保存添加观察者时传入了NotificationName的情况,
  2. GSIMapTable nameless 一张用于保存添加观察者时没有传入NotificationName的情况
Named Table

在named table中,NotificationName作为表的key,但因注册观察者的时候候,可以传入一个object参数,用于接收指定对象发出的通知,并且一个通知可注册多个观察者,所以还需要一张表来保存object和observer的对应关系。这张表以object为key,observer为value。
如何实现同一个通知对应多个观察者的情况?
就是用链表的数据结构。
具体结构如下图所示:

NameTable

named table最终的数据结构如上图所示:

  • 外层是一个table,以通知名称NotificationName为key,其value为一个table(简称内层table)
  • 内层table以object为key,其value为一个链表,用来保存所有的观察者

注意:在实际开发过程中object参数我们经常传nil,这时候系统会根据nil自动生成一个key,相当于这个key对应的value(链表)保存的就是当前通知传入了NotificationName没有传入object的所有观察者。当对应的NotificationName的通知发送时,链表中所有的观察者都会收到通知。

Nameless Table

Nameless Table比Named Table要简单很多,因为没有NotificationName作为key,直接用object作为key。相较于Named Table要少一层table嵌套。


image.png
wildcard

wildcard是链表的数据结构,如果在注册观察者时既没有传入NotificationName,也没有传入object,就会添加到wildcard的链表中。注册这里的观察者能收到所有的系统通知。

添加观察者的流程

从上面基本的结构关系,再去看一下添加的过程就会很简单了。

  1. 在初始化NotificationCenter时会创建一个对象,这个对象里保存了 NameTable Nameless Table wildcard和一些其他信息
  2. 添加时都会调用addObserver:selector:name:object
  3. 根据传入的参数创建一个Observation实例,这个实例保存了观察者对象、回调方法、以及下一个Observation对象的地址
  4. 如果传入了NotificationName,则会以NotificationName为key去查找对应的value,如果找到value,则取出对应的value;如果没有找到对应的value,则新建一个table,然后将这个table以NotificationName为key添加到Named Table中
  5. 在第4步中,找到的那个table,是存储object和Observation关系的表,以object为key取对应的Observation的链表。如果找到了,则将第3步创建的Observation实例插入到那个链表的尾部;如果没找到,Observation实例则作为链表的头节点。
  6. 如果没有传入NotificationName但是传入了object,会直接根据对应的object为key去找对应的链表
  7. 如果既没有传入NotificationName也没有传入object,则这个观察者会添加到wildcard链表中,头插法。

发送通知流程

发送通知一般调用postNotificationName:object:userInfo:来实现,内部会根据传入的参数实例化一个NSNotification对象,包含name、object、userinfo等信息。

我们明白了注册通知的流程,其实发送通知的流程也就很简单了,一个是存,一个是取。根据NotificationName和object找到对应的链表,然后遍历整个链表,给每个Observation节点中保存的observer发送对应的SEL消息。

具体流程如下:

  1. 首先会创建一个数组observerArr用来保存需要通知的observer
  2. 遍历wildcard链表,将observer添加到observerArray数组中
  3. 若存在object,但不存在NotificationName,就会在NameLess Table中找到以object为key的链表,然后遍历找到的链表,将observer添加到observerArr数组中
  4. 若存在NotificationName,在named table中以NotificationName为key找到对应的table,然后再在找到的table中以object为key找到对应的链表,遍历链表,将observer添加到observerArray数组中,然后再以nil为key找到对应的链表,遍历链表,将observer添加到observerArray数组中。
  5. 至此所有关于当前通知的observer(wildcard+nameless+named)都已经加入到了数组observerArray中。遍历observerArray数组,取出其中的observer节点(包含了观察者对象和selector),使用performSelector 进行调用:
[o->observer performSelector: o->selector withObject: notification];

这种处理通知的方式也就能说明,发送通知的线程和接收通知的线程是同一线程

移除通知流程

根据前面分析的添加观察者的流程与发送通知的流程可以类比出移除通知的流程。

  1. 如果NotificationName和object都为nil,则清空wildcard链表
  2. 如果NotificationName为nil,遍历named table,如果object为nil,则清空named table,如果object不为nil,则以object为key找到对应的链表,然后清空链表。在nameless table中以object为key找到对应的observer链表,然后清空,如果object为nil,清空nameless table
  3. 若NotificationName不为nil,在named table中以NotificationName为key找到对应的table,若object为nil,则清空找到的table,若object不为nil,则以object为key在找到的table中取出对应的链表,然后清空链表。
    GUNStep源码,可以从里面看到NSNotificationCenter的具体实现,不过是作为参考
    Gunstep NSNotififcationCenter sourcecode
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容