什么?你以为你通知全懂了?抱歉,你懂的仅仅是基础。
一、为什么要使用NSNotification
The standard way to pass information between objects is message passing—one object invokes the method of another object. However, message passing requires that the object sending the message know who the receiver is and what messages it responds to. At times, this tight coupling of two objects is undesirable—most notably because it would join together two otherwise independent subsystems. For these cases, a broadcast model is introduced: An object posts a notification, which is dispatched to the appropriate observers through an NSNotificationCenter object, or simply notification center.
大概意思是,若想objectA需要调用objectB的一个方法,一般来说,需要objectA中能访问到objectB,但是这是一种不受欢迎的会造成耦合的方式。所以,引入了一个新机制,叫NSNotificationCenter,由NSNotificationCenter管理objectB等注册的object,而objectA不需要知道objectB,只需要通过标记(例如NotificationName)在NSNotificationCenter中找到对应的object,从而调用该object的方法。无疑,这是一种松耦合,并且允许多对多的一种方式。
通知和delegate的基本区别:
通知是允许多对多的,而delegate只能是1对1的。
通知是松耦合的,通知方不需要知道被通知方的任何情况,而delegate不行。
通知的效率比起delegate略差。
二、通知的基本使用
1、通知中的基本概念
NSNotification
// NSNotification.h相关代码 @interface NSNotification : NSObject @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_AVAILABLE(10_6, 4_0) NS_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER; @end @interface NSNotification (NSNotificationCreation) + (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 */ @end // 理解 NSNotification作为一种消息传递的媒介,包含三个public成员变量,通过NSNotificationName类型的name来查找对应observer,并且可以在object和userInfo中传入参数。可以使用上述的几种初始化方式进行初始化。
NSNotificationCenter
// 默认的通知中心,全局只有一个 @property (class, readonly, strong) NSNotificationCenter *defaultCenter; // 在通知中心添加观察者 - (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject; // 向通知中心发送通知 - (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; // iOS4以后,以block的形式代替selector方式为通知中心添加观察者 - (id )addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block NS_AVAILABLE(10_6, 4_0);
NSDistributedNotificationCenter
// 在iOS的框架中找不到这个类相关知识。该通知中心是Mac OS中,进程间通知使用。不多进行介绍。
2、通知的基本使用
代码
// 代码 #import "AppDelegate.h" @interface A : NSObject - (void)test; @end @implementation A static int count = 0; - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)test { // 观察方式A:selector方式 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(xxx) name:@"111" object:nil]; // 观察方式B:block方式(queue参数决定你想把该block在哪一个NSOperationQueue里面执行) [[NSNotificationCenter defaultCenter] addObserverForName:@"111" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { NSLog(@"block %d", ++count); }]; } - (void)xxx { NSLog(@"selector %d", ++count); } @end @interface AppDelegate () @property (nonatomic, strong) A *a; @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { A *a = [A new]; [a test]; self.a = a; // 发送方式A:手动定义NSNotification NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil]; [[NSNotificationCenter defaultCenter] postNotification:noti]; // 发送方式B:自动定义NSNotification [[NSNotificationCenter defaultCenter] postNotificationName:@"111" object:nil userInfo:nil]; return YES; } @end // 输出 2017-02-26 19:00:27.461 notification[14092:12661907] selector 1 2017-02-26 19:00:27.462 notification[14092:12661907] block 2 2017-02-26 19:00:27.462 notification[14092:12661907] selector 3 2017-02-26 19:00:27.462 notification[14092:12661907] block 4
理解
两个观察者,两个发送者,可见有四个log信息,可以说明,通知支持这种多对多的消息传递。
3、疑难点
同步or异步
同步和异步都是相对于发送通知所在的线程的。可以通过下述代码简单测出:
// 替换上述的didFinishLaunchingWithOptions函数 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { A *a = [A new]; [a test]; self.a = a; // 发送方式A:手动定义NSNotification NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil]; [[NSNotificationCenter defaultCenter] postNotification:noti]; // 发送方式B:自动定义NSNotification [[NSNotificationCenter defaultCenter] postNotificationName:@"111" object:nil userInfo:nil]; NSLog(@"测试同步还是异步"); return YES; }
输出总是为:
2017-02-26 19:13:04.739 notification[15240:12674807] selector 1 2017-02-26 19:13:04.743 notification[15240:12674807] block 2 2017-02-26 19:13:04.743 notification[15240:12674807] selector 3 2017-02-26 19:13:04.744 notification[15240:12674807] block 4 2017-02-26 19:13:04.744 notification[15240:12674807] 测试同步还是异步
可以看出,postNotification:总是会卡住当前线程,待observer执行(如不特殊处理selector也会在postNotification:所在线程执行)结束之后才会继续往下执行。所以是同步的。
忘记remove的问题
这个就不进行测试了,因为我也没有iOS8以及之前的设备。直接写结论了:
若在iOS8或之前版本系统中,对一个对象addObserver:selector:name:object:(假设name为@“111”),但是并没有在dealloc的之前或之中,对其进行remove操作。那么,在发送通知(name为@“111”)的时候,会因为bad_access(野指针)而crash。
若在iOS9及以后,同上操作,不会crash。
iOS8及以前,NSNotificationCenter持有的是观察者的unsafe_unretained指针(可能是为了兼容老版本),这样,在观察者回收的时候未removeOberser,而后再进行post操作,则会向一段被回收的区域发送消息,所以出现野指针crash。而iOS9以后,unsafe_unretained改成了weak指针,即使dealloc的时候未removeOberser,再进行post操作,则会向nil发送消息,所以没有任何问题。
三、Notification Queues和异步通知
1、异步通知的原理
创建一个NSNotificationQueue队列(first in-first out),将定义的NSNotification放入其中,并为其指定三种状态之一:
typedef NS_ENUM(NSUInteger, NSPostingStyle) { NSPostWhenIdle = 1, // 当runloop处于空闲状态时post NSPostASAP = 2, // 当当前runloop完成之后立即post NSPostNow = 3 // 立即post,同步(为什么需要这种type,且看三.3) };
这样,将NSNotification放入queue,然后根据其type,NSNotificationQueue在合适的时机将其post到NSNotificationCenter。这样就完成了异步的需求。
2、异步通知的使用
// 替换上述的didFinishLaunchingWithOptions函数 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { A *a = [A new]; [a test]; self.a = a; NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil]; [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP]; //[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle]; //[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostNow]; NSLog(@"测试同步还是异步"); return YES; } // 输出 2017-02-26 19:56:32.805 notification[19406:12719309] 测试同步还是异步 2017-02-26 19:56:32.816 notification[19406:12719309] selector 1 2017-02-26 19:56:32.816 notification[19406:12719309] block 2
上述的输出可以看出,这样确实完成了异步通知的需求。当然,如果将type改成NSPostNow,则还是同步执行,效果和不用NSNotificationQueue相同。
3、Notification Queues的合成作用
NSNotificationQueue除了有异步通知的能力之外,也能对当前队列的通知根据NSNotificationCoalescing类型进行合成(即将几个合成一个)。
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) { NSNotificationNoCoalescing = 0, // 不合成 NSNotificationCoalescingOnName = 1, // 根据NSNotification的name字段进行合成 NSNotificationCoalescingOnSender = 2 // 根据NSNotification的object字段进行合成 };
可以通过以下两种方式进行对比:
// 方式1: - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { A *a = [A new]; [a test]; self.a = a; NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil]; [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP coalesceMask:NSNotificationNoCoalescing forModes:nil]; [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:nil]; [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostNow coalesceMask:NSNotificationNoCoalescing forModes:nil]; NSLog(@"测试同步还是异步"); return YES; } // 方式2: - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { A *a = [A new]; [a test]; self.a = a; NSNotification *noti = [NSNotification notificationWithName:@"111" object:nil]; [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:nil]; [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle coalesceMask:NSNotificationCoalescingOnName forModes:nil]; [[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostNow coalesceMask:NSNotificationCoalescingOnName forModes:nil]; NSLog(@"测试同步还是异步"); return YES; } // 输出 // 方式一 2017-02-26 20:09:31.834 notification[20612:12733161] selector 1 2017-02-26 20:09:31.835 notification[20612:12733161] block 2 2017-02-26 20:09:31.835 notification[20612:12733161] 测试同步还是异步 2017-02-26 20:09:31.851 notification[20612:12733161] selector 3 2017-02-26 20:09:31.851 notification[20612:12733161] block 4 2017-02-26 20:09:31.854 notification[20612:12733161] selector 5 2017-02-26 20:09:31.855 notification[20612:12733161] block 6 // 方式二 2017-02-26 20:11:31.186 notification[20834:12736113] selector 1 2017-02-26 20:11:31.186 notification[20834:12736113] block 2 2017-02-26 20:11:31.186 notification[20834:12736113] 测试同步还是异步
大概完成了合成的需求,但是此处还有一个疑问,比如三个通知合成一个,那么实际发送的NSNotification到底是怎样的呢?留给读者们自己挖掘吧。
四、指定Thread处理通知
需求
假设将didFinishLaunchingWithOptions改为:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { A *a = [A new]; [a test]; self.a = a; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); dispatch_async(queue, ^{ NSLog(@"current thread %@", [NSThread currentThread]); [[NSNotificationCenter defaultCenter] postNotificationName:@"111" object:nil]; }); return YES; }
可知,a中的observer的selector将会在DISPATCH_QUEUE_PRIORITY_BACKGROUND中执行,若该selector执行的是刷新UI的操作,那么这种方式显然是错误的。这里,我们需要保证selector永远在mainThread执行。所以,有以下两种方式,指定observer的回调方法的执行线程。
解决方式1:NSMachPort
// 代码 #import @interface A : NSObject @property NSMutableArray *notifications; @property NSThread *notificationThread; @property NSLock *notificationLock; @property NSMachPort *notificationPort; - (void)setUpThreadingSupport; - (void)handleMachMessage:(void *)msg; - (void)processNotification:(NSNotification *)notification; - (void)test; @end @implementation A - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)test { [self setUpThreadingSupport]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"111" object:nil]; } - (void)setUpThreadingSupport { if (self.notifications) { return; } self.notifications = [[NSMutableArray alloc] init]; self.notificationLock = [[NSLock alloc] init]; self.notificationThread = [NSThread mainThread]; self.notificationPort = [[NSMachPort alloc] init]; [self.notificationPort setDelegate:self]; [[NSRunLoop currentRunLoop] addPort:self.notificationPort forMode:(NSString *)kCFRunLoopCommonModes]; } - (void)handleMachMessage:(void *)msg { [self.notificationLock lock]; while ([self.notifications count]) { NSNotification *notification = [self.notifications objectAtIndex:0]; [self.notifications removeObjectAtIndex:0]; [self.notificationLock unlock]; [self processNotification:notification]; [self.notificationLock lock]; }; [self.notificationLock unlock]; } - (void)processNotification:(NSNotification *)notification { if ([NSThread currentThread] != self.notificationThread) { [self.notificationLock lock]; [self.notifications addObject:notification]; [self.notificationLock unlock]; [self.notificationPort sendBeforeDate:[NSDate date] components:nil from:nil reserved:0]; } else { NSLog(@"current thread %@ 刷新UI", [NSThread currentThread]); // 刷新UI ... } } @end // 输出 2017-02-27 11:49:04.296 notification[29036:12827315] current thread {number = 3, name = (null)} 2017-02-27 11:49:04.307 notification[29036:12827268] current thread {number = 1, name = main} 刷新UI
由输出可知,虽然post不是在主线程,但是刷新UI确实是在主线程,达成需求。这是官方文档提供的一种方式,就不具体解释了,也很容易看懂。不懂去看文档吧。
解决方式2:block方式addObserver
文档提供的方式一难用且冗长,这里我们可以用下述方式替代。
// 代码 @interface A : NSObject - (void)test; @end @implementation A - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)test { [[NSNotificationCenter defaultCenter] addObserverForName:@"111" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { NSLog(@"current thread %@ 刷新UI", [NSThread currentThread]); // 刷新UI ... }]; } @end // 输出 current thread {number = 3, name = (null)} 2017-02-27 11:53:46.531 notification[29510:12833116] current thread {number = 1, name = main} 刷新UI
完美!方便!
五、通知实现解析
这里我参见的是伪源码。
源码很多,不一一解释,我关注的只有两个点:
observer的对象存储在何处?
post的时候根据何种方式查找接受通知的对象?
observer的对象存储在何处
// NSNotificationCenter的init方法 - (id) init { if ((self = [super init]) != nil) { _table = newNCTable(); } return self; }
这也就很容易看出,每个NSNotificationCenter都有一个默认的_table。其对observer进行引用(iOS9以前unsafe_unretained,iOS9以后weak)。
post的时候根据何种方式查找接受通知的对象
// postNotification的部分代码 /* * Find the observers that specified OBJECT, but didn't specify NAME. */ if (object) { n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object); if (n != 0) { o = purgeCollectedFromMapNode(NAMELESS, n); while (o != ENDOBS) { GSIArrayAddItem(a, (GSIArrayItem)o); o = o->next; } } } /* * Find the observers of NAME, except those observers with a non-nil OBJECT * that doesn't match the notification's OBJECT). */ if (name) { n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name)); if (n) { m = (GSIMapTable)n->value.ptr; } else { m = 0; } if (m != 0) { /* * First, observers with a matching object. */ n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object); if (n != 0) { o = purgeCollectedFromMapNode(m, n); while (o != ENDOBS) { GSIArrayAddItem(a, (GSIArrayItem)o); o = o->next; } } if (object != nil) { /* * Now observers with a nil object. */ n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil); if (n != 0) { o = purgeCollectedFromMapNode(m, n); while (o != ENDOBS) { GSIArrayAddItem(a, (GSIArrayItem)o); o = o->next; } } } } }
不用完全看懂这块代码,只需要看出,在table中查找observer object的时候,首先根据的是object,接下来根据的是name,可见name的优先级比较高。
六、拾遗
1、那些系统的通知Name
// 当程序被推送到后台时 UIKIT_EXTERN NSNotificationName const UIApplicationDidEnterBackgroundNotification NS_AVAILABLE_IOS(4_0); // 当程序从后台将要重新回到前台时 UIKIT_EXTERN NSNotificationName const UIApplicationWillEnterForegroundNotification NS_AVAILABLE_IOS(4_0); // 当程序完成载入后通知 UIKIT_EXTERN NSNotificationName const UIApplicationDidFinishLaunchingNotification; // 应用程序转为激活状态时 UIKIT_EXTERN NSNotificationName const UIApplicationDidBecomeActiveNotification; // 用户按下主屏幕按钮调用通知,并未进入后台状态 UIKIT_EXTERN NSNotificationName const UIApplicationWillResignActiveNotification; // 内存较低时通知 UIKIT_EXTERN NSNotificationName const UIApplicationDidReceiveMemoryWarningNotification; // 当程序将要退出时通知 UIKIT_EXTERN NSNotificationName const UIApplicationWillTerminateNotification; // 当系统时间发生改变时通知 UIKIT_EXTERN NSNotificationName const UIApplicationSignificantTimeChangeNotification; // 当StatusBar框方向将要变化时通知 UIKIT_EXTERN NSNotificationName const UIApplicationWillChangeStatusBarOrientationNotification __TVOS_PROHIBITED; // userInfo contains NSNumber with new orientation // 当StatusBar框方向改变时通知 UIKIT_EXTERN NSNotificationName const UIApplicationDidChangeStatusBarOrientationNotification __TVOS_PROHIBITED; // userInfo contains NSNumber with old orientation // 当StatusBar框Frame将要改变时通知 UIKIT_EXTERN NSNotificationName const UIApplicationWillChangeStatusBarFrameNotification __TVOS_PROHIBITED; // userInfo contains NSValue with new frame // 当StatusBar框Frame改变时通知 UIKIT_EXTERN NSNotificationName const UIApplicationDidChangeStatusBarFrameNotification __TVOS_PROHIBITED; // userInfo contains NSValue with old frame // 后台下载状态发生改变时通知(iOS7.0以后可用) UIKIT_EXTERN NSNotificationName const UIApplicationBackgroundRefreshStatusDidChangeNotification NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED; // 受保护的文件当前变为不可用时通知 UIKIT_EXTERN NSNotificationName const UIApplicationProtectedDataWillBecomeUnavailable NS_AVAILABLE_IOS(4_0); // 受保护的文件当前变为可用时通知 UIKIT_EXTERN NSNotificationName const UIApplicationProtectedDataDidBecomeAvailable NS_AVAILABLE_IOS(4_0); // 截屏通知(iOS7.0以后可用) UIKIT_EXTERN NSNotificationName const UIApplicationUserDidTakeScreenshotNotification NS_AVAILABLE_IOS(7_0);
2、通知的效率
该用就用,不用特别在意。
七、文献
1、https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Notifications/Introduction/introNotifications.html#//apple_ref/doc/uid/10000043-SW1
2、https://github.com/gnustep/base/tree/master/Source