NSNotification学习

主要分为NSNotification、NSNotificationCenter和底层队列NSNotificationQueue。

优点:跨层通信、解耦。

使用注意点:尽量将所有的通知名和观察对象的关联关系放置在独立文件中,方便维护与查找。

NSNotification

包含了通知名name发送通知的对象object以及保存信息userInfo
其中,NSNotificationName作为区分通知的存在。

一般通过NSNotificationCenter的相关API来自动创建通知对象,无需手动创建(NSNotification是类簇,调用init初始化会抛异常)。

NSNotificationCenter

系统默认为APP通过实现了名为defaultCenter的单例通知中心对象。所有的通知(跨层,跨线程)默认都通过此对象进行通知的分发。

系统收到post通知请求时,会扫描注册到NSNotificationCenter对象中的所有观察者分发表。因此在大量频繁使用通知进行通信时,可以考虑使用自定义的NSNotificationCenter对象来提高性能。

系统提供了target+selectorblock+queue两种方式来向NSNotificationCenter注册观察者。

默认在哪个线程发送通知(post),就在哪个线程执行回调。除非通过block的方式发送通知,指定执行的队列。

注意

在block+queue的方式中,由于执行时机的不同(runloop的不同时机)NSNotificationCenter会将block拷贝到堆中进行保留。因此需要注意对象的捕获即内存引用循环问题。

NSNotificationCenter的同步执行

不管接收通知的观察者回调在什么线程上执行,通过NSNotificationCenter调用postNotification时都是同步执行的

- (void)viewDidLoad {
    [super viewDidLoad];

    // 主线程监听
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(testSelector1) name:@"jiji.notification.test" object:nil];
   
   // 后台线程监听
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(testSelector1) name:@"jiji.notification.test" object:nil];
    });
    
    // 主线程监听,block方式,强制在主队列执行(主线程)
    [[NSNotificationCenter defaultCenter] addObserverForName:@"jiji.notification.test" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"%s", __func__);
        NSLog(@"thread - %@", [NSThread currentThread]);
    }];
    
   
    // 在后台线程发送通知
    dispatch_queue_t queue = dispatch_queue_create("com.jiji.queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), queue, ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"jiji.notification.test" object:nil];
        NSNotificationQueue
        
        NSLog(@"post finish----");
    });
}

- (void)testSelector1 {
    NSLog(@"%s", __func__);
    NSLog(@"thread - %@", [NSThread currentThread]);
}

执行结果:

2019-09-05 13:39:36.594370+0800 TestAPP[52913:2455622] -[ViewController testSelector1]
2019-09-05 13:39:36.594797+0800 TestAPP[52913:2455622] thread - <NSThread: 0x6000001bd640>{number = 4, name = (null)}
2019-09-05 13:39:36.595147+0800 TestAPP[52913:2455622] -[ViewController testSelector1]
2019-09-05 13:39:36.595537+0800 TestAPP[52913:2455622] thread - <NSThread: 0x6000001bd640>{number = 4, name = (null)}
2019-09-05 13:39:36.596740+0800 TestAPP[52913:2455552] -[ViewController viewDidLoad]_block_invoke
2019-09-05 13:39:36.597015+0800 TestAPP[52913:2455552] thread - <NSThread: 0x6000001d1f00>{number = 1, name = main}
2019-09-05 13:39:36.597354+0800 TestAPP[52913:2455622] post finish----

可以看到:

  1. 除block版本监听外,其他的通知回调执行线程都与发送通知的线程相同(保证线程安全)。
  2. NSNotificationCenter主动发送通知为同步操作:在所有监听者的回调执行结束之后,被阻塞的发送操作才向下继续执行。因此,对于消耗性能或长时间的操作需要尽量避免。

发送同步通知的过程,就是直接的消息发送

NSNotificationQueue的异步调用

那如果监听方需要执行的任务确实需要长时间怎么办?除了可以在回调中将任务放在子线程中运行(GCD或NSOperationQueue),Foundation框架本身也提供了一个异步发送通知的方式,也就是NSNotificationQueue。

NSNotificationQueue,顾名思义,是一个保存着NSNotification的FIFO队列。

每个线程都有自己默认的NSNotificationQueue对象,可以与NSNotificationCenter进行关联,通过center异步发送通知

  • NSNotificationQueue可以根据线程所属的runloop确定通知发送的时机:
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空闲时发送
    NSPostASAP = 2, // runloop中尽快发送(as soon as possible)
    NSPostNow = 3 // 实时发送(直接通过NSNotificationCenter发送)
};
  • NSNotificationQueue可以根据类型将相同的通知进行合并发送。
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0, // 不合并
    NSNotificationCoalescingOnName = 1, // 同名通知合并
    NSNotificationCoalescingOnSender = 2 // 相同object合并
};

完整类定义如下:

@interface NSNotificationQueue : NSObject {
@private
    // 关联的NSNotificationCenter对象
    id  _notificationCenter; 
    // runloop尽快发送队列
    id  _asapQueue;
    // 尽快发送队列对应的首个观察者
    id  _asapObs;
    // runloop空闲时刻队列
    id  _idleQueue;
    // 空闲时刻队列对应的首个观察者
    id  _idleObs;
}

/** 默认队列 */
@property (class, readonly, strong) NSNotificationQueue *defaultQueue;

/** 绑定NSNotificationCenter对象并初始化自身 */
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER;

/** NSNotification入队,并指定发送时机(根据时机进入不同的私有队列) */
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
/** 针对指定的runloop的mode,NSNotification入队,并指定发送时机及合并方式 */
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;

/** NSNotification出队,并指定合并方式 */
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;

@end

根据实现,根据私有成员变量,可以猜想一下:

  1. NSNotificationQueue可能只是一个对象封装,内部的_asapQueue和_idleQueue才是真正的队列容器。根据入队NSNotification对象时设置的配置信息,插入到相应的队列中。
  2. 在NSNotification对象出队准备执行时,需要在绑定的NSNotificationCenter的分发表中查询出通知名对应的观察者信息(应该包含观察者对象及回调SEL),赋值给如_asapObs的观察者信息指针,runloop则通过此观察者进行通知回调。

作为对比,看个发送异步通知的例子:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(testSelector1) name:@"jiji.notification.test" object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(testSelector2) name:@"jiji.notification.test" object:nil];

    // 强制主线程执行
    [[NSNotificationCenter defaultCenter] addObserverForName:@"jiji.notification.test" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        NSLog(@"%s", __func__);
        NSLog(@"thread - %@", [NSThread currentThread]);
    }];
    
    
    dispatch_async(
        dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
        ^{
            // 在runloop的mcurrentMode中插入item(source1),防止线程退出
            [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSRunLoopCommonModes];

            // 在子线程发送异步通知(异步通知依赖runloop)
            NSNotification *noti = [NSNotification notificationWithName:@"jiji.notification.test" object:nil];
            [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];

            NSLog(@"post finish---");

            [[NSRunLoop currentRunLoop] run];
        }
    );
}

- (void)testSelector1 {
    NSLog(@"%s", __func__);
    NSLog(@"thread - %@", [NSThread currentThread]);
}

- (void)testSelector2 {
    NSLog(@"%s", __func__);
    NSLog(@"thread - %@", [NSThread currentThread]);
}

执行结果:

2019-09-05 16:48:29.744888+0800 TestAPP[56766:2652270] post finish---
2019-09-05 16:48:29.745274+0800 TestAPP[56766:2652270] -[ViewController testSelector1]
2019-09-05 16:48:29.745815+0800 TestAPP[56766:2652270] thread - <NSThread: 0x6000015193c0>{number = 5, name = (null)}
2019-09-05 16:48:29.746069+0800 TestAPP[56766:2652270] -[ViewController testSelector2]
2019-09-05 16:48:29.746229+0800 TestAPP[56766:2652270] thread - <NSThread: 0x6000015193c0>{number = 5, name = (null)}
2019-09-05 16:48:29.825634+0800 TestAPP[56766:2652197] -[ViewController viewDidLoad]_block_invoke
2019-09-05 16:48:29.841702+0800 TestAPP[56766:2652197] thread - <NSThread: 0x600001562c00>{number = 1, name = main}

可以看到,在将NSNotification对象入队后,方法直接返回,继续向下执行。而通知则通过runloop在适当的时刻进行发出

其中,若修改入队参数coalesceMask为NSNotificationCoalescingOnName,即可实现对于回调频率的控制(高频多次调用只发送一个通知)。对于参数postingStyle,若设置为NSPostNow,则为同步消息发送,与postNotification相同。

而且,通过调用栈信息可以看出,发送异步通知的过程,实际上是在线程runloop的指定mode中插入了一个Observer的item,待需要执行时进行调用

NSNotification与KVO的区别

KVO是在对象的keyPath上添加观察者的,而NSNotification是在通知名上添加观察者。

NSNotification使用范围广,更加灵活,只不过需要主动发送通知才能触发给观察者。

参考资料:

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

推荐阅读更多精彩内容