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;
主要是以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、通知的移除 页面销毁时不移除通知会崩溃吗?
- addObserver添加的通知在iOS 9.0之前,通知中心对观察者对象进行unsafe_unretained 引用,当被引用的对象释放时不会自动置为nil,,也就是成了野指针,需要在dealloc手动移除。
iOS 9.0之后通知中心对观察者做了弱引用,当被添加通知的对象销毁的时候,通知会自动被移除。。 - 但 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] 进入睡眠