iOS NSNotification相关

1、通知实现原理(结构设计、通知如何存储的、name&observer&SEL之间的关系等)

  • NSNotification 通知的模型 name、object、userinfo.
  • NSNotificationCenter通知中心 负责发送NSNotification
  • NSNotificationQueue通知队列 负责在某些时机触发 调用NSNotificationCenter通知中心 post通知

通知是结构体通过双向链表进行数据存储

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
  Observation       *wildcard;  /* 链表结构,保存既没有name也没有object的通知 */
  GSIMapTable       nameless;   /* 存储没有name但是有object的通知 */
  GSIMapTable       named;      /* 存储带有name的通知,不管有没有object  */
    ...
} NCTable;

// Observation 存储观察者和响应结构体,基本的存储单元
typedef struct  Obs {
  id        observer;   /* 观察者,接收通知的对象  */
  SEL       selector;   /* 响应方法     */
  struct Obs    *next;      /* Next item in linked list.    */
  ...
} Observation;
namelsee
name

主要是以key value的形式存储,这里需要重点强调一下 通知以 name和object两个纬度来存储相关通知内容,也就是我们添加通知的时候传入的两个不同的方法.
简单理解name&observer&SEL之间的关系就是name作为key, observer作为观察者对象,当合适时机触发就会调用observer的SEL.

2、通知的添加方式

[[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(receiceNotification:)
                                                 name:@"JKRNO"
                                               object:nil];

addObserver:接收通知的对象
selector:接收通知的对象接收到通知调用的方法
name:通知的名字
object:接收哪个对象发送的通知

@property (nonatomic, strong) NSObject *observer;
...
    self.observer = [[NSNotificationCenter defaultCenter]
                    addObserverForName:@"JKRSEC"
                    object:self 
                    queue:[NSOperationQueue new]
                    usingBlock:^(NSNotification * _Nonnull note) {
                        /// 接收到通知回调的block
                    }];

返回值:通知实际添加到的observer,移除通知要移除这个对象
name参数:通知的名字
object:接收哪个对象发送的通知
queue:接收到通知的回调在哪个线程中调用,如果传mainQueue则通知在主线程回调,否则在子线程回调
usingBlock:接收到通知回调的block

3、通知的移除 页面销毁时不移除通知会崩溃吗?

  1. addObserver添加的通知在iOS 9.0之前,通知中心对观察者对象进行unsafe_unretained 引用,当被引用的对象释放时不会自动置为nil,,也就是成了野指针,需要在dealloc手动移除。
    iOS 9.0之后通知中心对观察者做了弱引用,当被添加通知的对象销毁的时候,通知会自动被移除。。
  2. 但 addObserverForName,被系统 retain,手动移除通知,同时这个 block类型参数也需注意避免循环引用。最明显的体现就是,就算你的ViewController被释放了,走了dealloc,第二次进入VC中会执行两次block中的代码块。

4、通知的发送时同步的,还是异步的?

- (void)textNotifation {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notifyHandle) name:@"NotificationTestName" object:nil];
    NSLog(@"即将发出通知");
    [[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationTestName" object:nil];
    NSLog(@"处理发出通知的下一条代码");
}

- (void)notifyHandle {
    sleep(10);
    NSLog(@"通知处理结束");
}

以上示例证明通知的发送和接收和同步的,即通知发送后,在通知接收方法完成之前,通知发送之后的代码会等待执行
默认在哪个线程发送通知,就在哪个线程接收到。

5、如何让通知异步的方法?

1、将通知的发送放到子线程中

 NSLog(@"即将发出通知");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationTestName" object:nil];
    });
    NSLog(@"处理发出通知的下一条代码");

2、将通知的处理方法放到子线程中

- (void)notifyHandle {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(10);
        NSLog(@"通知处理结束");
    });
}

3、通知的发送可以添加到NSNotificationQueue异步通知缓冲队列中

    NSLog(@"即将发出通知");
    NSNotification *notification = [NSNotification notificationWithName:XYNotificationTestName object:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
    NSLog(@"处理发出通知的下一条代码");

NSNoticicationQueue是一个通知缓冲队列,以FIFO(先进先出)的规则维护通知队列的发送。
向通知队列中添加通知有三种枚举类型:
1,NSPostWhenIdle:runloop空闲的时候回调通知方法
2,NSPostASAP:runloop能够调用的时候就回调通知方法
3,NSPostNow:runloop立即回调通知方法

  • 1、接收通知的线程,和发送通知所处的线程是同一个线程,和在哪个线程注册通知无关。
  • 2、遍历observerArray数组,取出其中的observer节点[o->observer performSelector: o->selector withObject: notification],所以通知是同步处理的机制。
  • 3、如果想改同步为异步,在收到通知方法中新开辟一个线程处理事件。
  • 4、如果在子线程接受通知并更新UI 会造成crash Main Thread Checker: UI API called on a background thread: -[UIButton setTitle:forState:],要么回到主线程update UI,要么在主线程发送通知

6、下面的方式能接收到通知吗?多次添加同一个通知会是什么结果?多次移除通知呢?

// 监听通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 发送通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
  • 1、如果发送的通知指定了object对象,那么观察者接收的通知设置的object对象与其一样,才会接收到通知,
  • 2、但是接收通知如果将这个参数设置为了nil,则会接收一切通知。
  • 3、多次添加同一个通知会触发多次调用,多次移除通知也不会crash

7、如何保证通知接收的线程在主线程?

dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName: @"NotificationTestName"  object:self];
            });

- (void)notifyHandle {
    dispatch_async(dispatch_get_main_queue(), ^{
        sleep(10);
        NSLog(@"通知处理结束");
    });
}

如果在子线程接受通知并更新UI 会造成crash Main Thread Checker: UI API called on a background thread: -[UIButton setTitle:forState:],要么回到主线程update UI,要么在主线程发送通知

8、NSNotificationQueue和runloop的关系?

为了验证通知和runloop的关系,在主线程添加runloop的状态监听:
postringStyle参数就是定义通知调用和runloop状态之间关系。
该参数的三个可选参数:
1,NSPostWhenIdle:runloop空闲的时候回调通知方法
2,NSPostASAP:runloop能够调用的时候就回调通知方法
3,NSPostNow:runloop立即回调通知方法

- (void)runLoopNotification {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"进入runLoop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"处理timer事件");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"处理source事件");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"进入睡眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"被唤醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"退出");
                break;
            default:
                break;
        }
    });
    CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
    CFRelease(observer);
    [self addObserverForNotify];
}
- (void)addObserverForNotify {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(receiceNotification:)
                                                 name:@"JKRNO"
                                               object:nil];
    [self postNotification];
}
- (void)postNotification {
    NSLog(@"1");
    NSNotification *notification = [NSNotification notificationWithName:@"JKRNO" object:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle];
    NSLog(@"3");
}

- (void)receiceNotification:(NSNotification *)notification {
    sleep(3);
    NSLog(@"2");
}

用第一个参数NSPostWhenIdle的时候,通知发送的时候runloop和通知方法调用的顺序如下:可以看到,通知回调方法是等待到当下线程runloop进入睡眠状态才会调用。

2021-08-17 14:27:46.237979+0800 exercise[30888:2524325] 1
2021-08-17 14:27:46.238169+0800 exercise[30888:2524325] 3
2021-08-17 14:27:46.264124+0800 exercise[30888:2524325] 处理timer事件
2021-08-17 14:27:46.264326+0800 exercise[30888:2524325] 处理source事件
2021-08-17 14:27:46.264705+0800 exercise[30888:2524325] 处理timer事件
2021-08-17 14:27:46.264828+0800 exercise[30888:2524325] 处理source事件
2021-08-17 14:27:46.265067+0800 exercise[30888:2524325] 进入睡眠
2021-08-17 14:27:49.266005+0800 exercise[30888:2524325] 2
2021-08-17 14:27:49.267331+0800 exercise[30888:2524325] 被唤醒

用第二个参数NSPostASAP的时候,通知发送的时候runloop和通知方法调用的顺序:可以看到,通知回调方法是等待到当下线程runloop开始接收事件源的时候就会调用。

2021-08-17 14:26:37.309870+0800 exercise[30867:2523325] 1
2021-08-17 14:26:37.310111+0800 exercise[30867:2523325] 3
2021-08-17 14:26:37.337183+0800 exercise[30867:2523325] 处理timer事件
2021-08-17 14:26:40.337853+0800 exercise[30867:2523325] 2
2021-08-17 14:26:40.338126+0800 exercise[30867:2523325] 处理source事件
2021-08-17 14:26:40.338555+0800 exercise[30867:2523325] 处理timer事件
2021-08-17 14:26:40.338684+0800 exercise[30867:2523325] 处理source事件
2021-08-17 14:26:40.339368+0800 exercise[30867:2523325] 处理timer事件
2021-08-17 14:26:40.339502+0800 exercise[30867:2523325] 处理source事件
2021-08-17 14:26:40.339652+0800 exercise[30867:2523325] 进入睡眠

用第三个参数NSPostNow的时候,通知发送的时候runloop和通知方法调用的顺序:其实和直接用默认的通知中心添加通知是一样的,通知马上调用回调方法。

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

推荐阅读更多精彩内容