方便记忆:
- 设计模式:观察者模式,实现对象间一对多的依赖关系
- NSNotification:方便 NSNotificationCenter 广播到其他对象的封装对象
- NSNotificationCenter:管理通知调度表中对象的发送和接收通知
- 发送时机:
- NSPostWhenIdle:空闲发送通知 (当运行循环处于等待或空闲状态时,发送通知,对于不重要的通知可以使用)
- NSPostASAP:尽快发送通知 (当前运行循环迭代完成时,通知将会被发送,有点类似没有延迟的定时器)
- NSPostNow :同步发送通知 (如果不使用合并通知和postNotification:一样是同步通知)
- 合并通知(系统的drawRect方法):
- NSNotificationNoCoalescing:不合并通知
- NSNotificationCoalescingOnName:合并相同名称的通知
- NSNotificationCoalescingOnSender:合并相同通知和同一对象的通知
- 异步通知:NSNotificationQueue实现对通知的控制管理
- 多线程通知:使用runloop维持多个线程使用NSPort进行通知(NSPort的runloop兼容mode)
前言
Cocoa 中使用 NSNotification、NSNotificationCenter 和 KVO 来实现观察者模式,实现对象间一对多的依赖关系。
本篇文章主要来讨论NSNotification、NSNotificationCenter、同步异步通知、通知中心底层原理
NSNotification、NSNotificationCenter 等
NSNotification
NSNotification 是方便 NSNotificationCenter 广播到其他对象的封装对象,通知中心(NSNotificationCenter)对通知调度表中的对象广播时发送NSNotification对象
@interface NSNotification : NSObject <NSCopying, NSCoding> @property (readonly, copy) NSNotificationName name; //标识通知的标记 @property (nullable, readonly, retain) id object; //要通知的对象 @property (nullable, readonly, copy) NSDictionary *userInfo; //存储发送通知时附带的信息
NSNotificationCenter
NSNotificationCenter是类似一个广播中心站,使用defaultCenter来获取应用中的通知中心,它可以向应用任何地方发送和接收通知。
在通知中心注册观察者,发送者使用通知中心广播时,以NSNotification的name和object来确定需要发送给哪个观察者。
为保证观察者能接收到通知,所以应先向通知中心注册观察者,接着再发送通知这样才能在通知中心调度表中查找到相应观察者进行通知。
发送者
发送者其实就是对post的使用,后面单独讲,发送通知可使用以下方法发送通知
- (void)postNotification:(NSNotification *)notification; - (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject; - (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
三种方式实际上都是发送NSNotification对象给通知中心注册的观察者。
发送通知通过name和object来确定来标识观察者,name和object两个参数的规则相同即当通知设置name为kChangeNotifition时,那么只会发送给符合name为kChangeNotifition的观察者
同理object指发送给某个特定对象通知
- 如果只设置了name,那么只有对应名称的通知会触发。
- 如果同时设置name和object参数时就必须同时符合这两个条件的观察者才能接收到通知。
观察者
你可以使用以下两种方式注册观察者
- (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 NS_AVAILABLE(10_6, 4_0);
- 第一种方式是比较常用的添加Oberver的方式,接到通知时执行aSelector。
- 第二种方式是基于Block来添加观察者,往通知中心的调度表中添加观察者,这个观察者包括一个queue和一个block,并且会返回这个观察者对象。当接到通知时执行block所在的线程为添加观察者时传入的queue参数,queue也可以为nil,那么block就在通知所在的线程同步执行。
这里需要注意的是如果使用第二种的方式创建观察者需要弱引用可能引起循环引用的对象,避免内存泄漏。
移除观察者
在对象被释放前需要移除掉观察者,避免已经被释放的对象还接收到通知导致崩溃。
移除观察者有两种方式:
- (void)removeObserver:(id)observer; - (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;
传入相应的需要移除的observer 或者使用第二种方式三个参数来移除指定某个观察者。
如果使用基于-[NSNotificationCenter addObserverForName:object:queue:usingBlock:]方法在获取方法返回的观察者进行释放。基于这个方法我们还可以让观察者接到通知后只执行一次:
__block __weak id<NSObject> observer = [[NSNotificationCenter defaultCenter] addObserverForName:kChangeNotifition object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { NSLog(@"-[NSNotificationCenter addObserverForName:object:queue:usingBlock:]"); [[NSNotificationCenter defaultCenter] removeObserver:observer]; }];
在iOS9中使用-[NSNotificationCenter addObserverForName:object:queue:usingBlock:]方法需要手动释放
NSNotificationQueue
NSNotificationQueue通知队列,用来管理多个通知的调用。通知队列通常以先进先出(FIFO)顺序维护通。
NSNotificationQueue就像一个缓冲池把一个个通知放进池子中,使用特定方式通过NSNotificationCenter发送到相应的观察者。下面我们会提到特定的方式即合并通知和异步通知。
1.创建通知队列方法:
- (instancetype)initWithNotificationCenter:(NSNotificationCenter *)notificationCenter NS_DESIGNATED_INITIALIZER; // 或者直接 defaultQueue NSNotificationQueue * notificationQueue = [NSNotificationQueue defaultQueue];
2.往队列加入通知方法:
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle; - (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSNotificationCoalescing)coalesceMask forModes:(nullable NSArray<NSRunLoopMode> *)modes;
3.移除队列中的通知方法:
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;
4.发送方式
控制通知发送的时机
NSPostingStyle包括三种类型:
typedef NS_ENUM(NSUInteger, NSPostingStyle) { NSPostWhenIdle = 1, NSPostASAP = 2, NSPostNow = 3 };
- NSPostWhenIdle:空闲发送通知 (当运行循环处于等待或空闲状态时,发送通知,对于不重要的通知可以使用)
- NSPostASAP:尽快发送通知 (当前运行循环迭代完成时,通知将会被发送,有点类似没有延迟的定时器)
- NSPostNow :同步发送通知 (如果不使用合并通知和postNotification:一样是同步通知)
5.合并通知
通过合并我们可以用来保证相同的通知只被发送一次
NSNotificationCoalescing包括三种类型:
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) { NSNotificationNoCoalescing = 0, NSNotificationCoalescingOnName = 1, NSNotificationCoalescingOnSender = 2 };
- NSNotificationNoCoalescing:不合并通知。
- NSNotificationCoalescingOnName:合并相同名称的通知。
- NSNotificationCoalescingOnSender:合并相同通知和同一对象的通知。
forModes:(nullable NSArray *)modes可以使用不同的NSRunLoopMode配合来发送通知,可以看出实际上NSNotificationQueue与RunLoop的机制以及运行循环有关系,通过NSNotificationQueue队列来发送的通知和关联的RunLoop运行机制来进行的。
通知管理:同(异)步、单(多)线程
同步通知
先写一个简单的通知示例
- (void)viewDidLoad { [super viewDidLoad]; //object:指定接受某个对象的通知,为nil表示可以接受任意对象的通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotifi:) name:@"EdisonNotif" object:nil]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self sendNotification]; } - (void)sendNotification { NSLog(@"发送通知before:%@", [NSThread currentThread]); [[NSNotificationCenter defaultCenter] postNotificationName:@"EdisonNotif" object:nil]; NSLog(@"发送通知after:%@", [NSThread currentThread]); } - (void)handleNotifi:(NSNotification*)notif { NSLog(@"接收到通知了:%@", [NSThread currentThread]); }
从 log 结果看出,通知处理都是同步的
发送通知before -> 接收到通知了 -> 发送通知after
说明消息发完之后要等处理了消息才跑发送消息之后的代码,这跟多线程中的同步概念相似
异步通知
发送完之后就继续执行下面的代码,不需要去等待接受通知的处理,这里用到通知对列 NSNotificationQueue
- (void)sendNotificationQueue { //每个线程都默认又一个通知队列,可以直接获取,也可以alloc NSNotificationQueue * notificationQueue = [NSNotificationQueue defaultQueue]; NSNotification * notification = [NSNotification notificationWithName:@"EdisonNotif" object:nil]; NSLog(@"异步发送通知before:%@",[NSThread currentThread]); [notificationQueue enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationCoalescingOnName forModes:nil]; NSLog(@"异步发送通知after:%@",[NSThread currentThread]); }
从 log 结果看出,通知处理实现了异步
发送通知before -> 发送通知after -> 接收到通知了
多线程通知
点击屏幕直接发送通知,开启一个线程发送通知
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ [NSThread detachNewThreadSelector:@selector(sendNotification) toTarget:self withObject:nil]; }
而用线程发送同步的是可以接受到通知的,并且处理也是在线程里处理的 ,这说通知队列跟线程是有关系的,再继续改代码,回到线程发送异步通知,只是把发送时机改成马上发送
[notificationQueue enqueueNotification:notification postingStyle:NSPostNow coalesceMask:NSNotificationNoCoalescing forModes:nil];
这又能处理通知,所以可以说NSPostNow就是同步,其实呢,[[NSNotificationCenter defaultCenter]通知中心这句代码的意思就是:你在哪个线程里面就是获取当前线程的通知队列并且默认采用NSPostNow发送时机
[notificationQueue enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:nil];
那么通知队列到底和线程有什么关系呢:每个线程都有一个通知队列,当线程结束了,通知队列就被释放了,所以当前选择发送时机为NSPostWhenIdle时也就是空闲的时候发送通知,通知队列就已经释放了,所以通知发送不出去了
如果线程不结束,就可以发送通知了,用runloop让线程不结束
- (void)sendAsyncNotification { //每个线程都默认又一个通知队列,可以直接获取,也可以alloc NSNotificationQueue * notificationQueue = [NSNotificationQueue defaultQueue]; NSNotification * notification = [NSNotification notificationWithName:@"EdisonNotif" object:nil]; NSLog(@"异步发送通知before:%@",[NSThread currentThread]); [notificationQueue enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:nil]; NSLog(@"异步发送通知after:%@",[NSThread currentThread]); NSPort * port = [NSPort new]; [[NSRunLoop currentRunLoop] addPort:port forMode:NSRunLoopCommonModes]; [[NSRunLoop currentRunLoop] run]; }
这样通知就被发送出去了,而且发送和处理也在线程中,这还没有达到真正的异步是吧,应该发送在一个线程,处理在另一个线程