iOS 通知详解
数据结构
从我们之前使用通知的流程和代码来看,通知其实就是一个单例,方便随时访问。
NSNotificationCenter:消息中心
这个单例类中主要定义了两个表,一个存储所有注册通知信息的表的结构体,一个保存单个注册信息的节点结构体。
typedef struct NCTbl {Observation *wildcard; // 添加观察者时既没有传入 NotificationName ,又没有传入object,就会加在这个链表上,它里边的观察者可以接收所有的系统通知GSIMapTable nameless; // 添加观察者时没有传入 NotificationName 的表GSIMapTable named; // 添加观察者时传入了 NotificationName 的表
} NCTable
观察者信息的结构:
typedef struct Obs {id observer; // 观察者对象SEL selector; // 方法信息struct Obs *next; // 指向下一个节点int retained; /* Retain count for structure. */struct NCTbl *link; /* Pointer back to chunk table */
} Observation;
named表
在 named 表中,NotifcationName 作为表的 key,因为我们在注册观察者的时候是可以传入一个参数 object 用于只监听指定该对象发出的通知,并且一个通知可以添加多个观察者,所以还需要一张表来保存 object 和 Observer 的对应关系。这张表的是 key、Value 分别是以 object 为 Key,Observer 为 value。用了链表这种数据结构实现保存多个观察者的情况。
在实际开发过程中 object 参数我们经常传 nil,这时候系统会根据 nil 自动生成一个 key,相当于这个 key 对应的 value(链表)保存的就是当前通知传入了 NotificationName 没有传入 object 的所有观察者。
同理nameless表和wildcard表如下:
添加观察者
使用方法addObserver:selector:name:object
添加观察者,根据 GNUStep 的源码分析:
- (void) addObserver: (id)observerselector: (SEL)selectorname: (NSString*)nameobject: (id)object
{Observation *list;Observation *o;GSIMapTable m;GSIMapNode n;
// observer为空时的报错if (observer == nil)[NSException raise: NSInvalidArgumentExceptionformat: @"Nil observer passed to addObserver ..."];
// selector为空时的报错if (selector == 0)[NSException raise: NSInvalidArgumentExceptionformat: @"Null selector passed to addObserver ..."];
// observer不能响应selector时的报错if ([observer respondsToSelector: selector] == NO){[NSException raise: NSInvalidArgumentExceptionformat: @"[%@-%@] Observer '%@' does not respond to selector '%@'",NSStringFromClass([self class]), NSStringFromSelector(_cmd),observer, NSStringFromSelector(selector)];}
// 给表上锁lockNCTable(TABLE);
// 建立一个新Observation,存储这次注册的信息o = obsNew(TABLE, selector, observer);// 如果有nameif (name) {// 在named表中 以name为key寻找valuen = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);// named表中没有找到对应的valueif (n == 0) {// 新建一个表m = mapNew(TABLE);// 由于这是对给定名称的首次观察,因此我们对该名称进行了复制,以便在map中无法对其进行更改(来自GNUStep的注释)name = [name copyWithZone: NSDefaultMallocZone()];// 新建表作为name的value添加在named表中GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);GS_CONSUMED(name)} else { //named表中有对应的value// 取出对应的valuem = (GSIMapTable)n->value.ptr;}// 将observation添加到正确object的列表中// 获取添加完后name对应的value的object对应的链表n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);// n是object的valueif (n == 0) { // 如果object对应value没有数据o->next = ENDOBS;// 将o作为object的value链表的头结点插入GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);} else { // 如果有object对应的value那么就直接添加到原练表的尾部// 在链表尾部加入olist = (Observation*)n->value.ptr;o->next = list->next;list->next = o;}// 这个else if 就是没有name有object的Observation,对object进行的操作相同,} else if (object) {// 直接获取object对应的value链表n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);if (n == 0) { // 这个对应链表如果没有数据o->next = ENDOBS;// 将该observation作为头节点插入GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);} else { // 有数据,将obsevation直接插在原链表的后面list = (Observation*)n->value.ptr;o->next = list->next;list->next = o;}} else {// 既没有name又没有object,就加在WILDCARD链表中o->next = WILDCARD;WILDCARD = o;}// 解锁unlockNCTable(TABLE);
}
流程
1.首先检查添加观察者方法的参数是否正确 ;
2.创建新的Observation存储这次注册的信息
3.先判断name是否存在,存在就把name作为key去named table查找value;没找到就新建表和对应的name作为key-value存入named table中;找到就获取value(object-observation的表) 然后这时在这个表中查找object ;没找到新建object-observation插入表中;找到先取出对应的observation链表,在链表尾部插入 ;
4.然后判断object是否存在,其他都和上面差不多,只不过这次是从nameless table中开始 ;
5.若既没有 NotificationName 也没有 object,那么就加在 WILDCARD 链表中。
发送通知
使用方法postNotification:
, postNotificationName:object:userInfo
或者postNotificationName:object:
发送通知,后者默认userInfo为nil
,同样使用GNUStep
源码进行分析:
- (void) postNotification: (NSNotification*)notification {if (notification == nil) {[NSException raise: NSInvalidArgumentExceptionformat: @"Tried to post a nil notification."];}[self _postAndRelease: RETAIN(notification)];
}- (void) postNotificationName: (NSString*)nameobject: (id)object {[self postNotificationName: name object: object userInfo: nil];
}- (void) postNotificationName: (NSString*)nameobject: (id)objectuserInfo: (NSDictionary*)info {GSNotification *notification;notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());notification->_name = [name copyWithZone: [self zone]];notification->_object = [object retain];notification->_info = [info retain];[self _postAndRelease: notification];
}
最终都只会调用_postAndRelease:
方法。
- (void) _postAndRelease: (NSNotification*)notification {Observation *o;unsigned count;NSString *name = [notification name];id object;GSIMapNode n;GSIMapTable m;GSIArrayItem i[64];GSIArray_t b;GSIArray a = &b;// name为空的报错,注册时可以注册无名,注册无名就等于说是所有的通知都能接收,但是发送通知时不可以if (name == nil) {RELEASE(notification);[NSException raise: NSInvalidArgumentExceptionformat: @"Tried to post a notification with no name."];}object = [notification object];GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);lockNCTable(TABLE);// 查找所有未指定name或object的观察者,加在a数组中,即将wildcard表中的数据都加在新建链表中for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next){GSIArrayAddItem(a, (GSIArrayItem)o);}// 查找与通知的object相同但是没有name的观察者,加在a数组中if (object) {// 在nameless中找object对应的数据节点n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);if (n != 0) { // 将其加入到新建链表中o = purgeCollectedFromMapNode(NAMELESS, n);while (o != ENDOBS) {GSIArrayAddItem(a, (GSIArrayItem)o);o = o->next;}}}// 查找name的观察者,但观察者的非零对象与通知的object不匹配时除外,加在a数组中if (name) {// 先匹配namen = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));if (n) { // m指向name匹配到的数据m = (GSIMapTable)n->value.ptr;} else {m = 0;}if (m != 0) { // 如果上述name查找到了数据// 首先,查找与通知的object相同的观察者n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);if (n != 0) { // 找到了与通知的object相同的观察者,就加入到新建链表中o = purgeCollectedFromMapNode(m, n);while (o != ENDOBS) {GSIArrayAddItem(a, (GSIArrayItem)o);o = o->next;}}if (object != nil) {// 接着是没有object的观察者,都加在新建链表中n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);if (n != 0) { // 如果没有object并且有数据,就把其中的数据加到新建链表中o = purgeCollectedFromMapNode(m, n);while (o != ENDOBS) {GSIArrayAddItem(a, (GSIArrayItem)o);o = o->next;}}}}}unlockNCTable(TABLE);// 发送通知,给之前新建链表中的所有数据count = GSIArrayCount(a);while (count-- > 0) {o = GSIArrayItemAtIndex(a, count).ext;if (o->next != 0) {NS_DURING {// 给observer发送selector,让其处理[o->observer performSelector: o->selectorwithObject: notification];}NS_HANDLER {BOOL logged;// 尝试将通知与异常一起报告,但是如果通知本身有问题,我们只记录异常。NS_DURINGNSLog(@"Problem posting %@: %@", notification, localException);logged = YES;NS_HANDLERlogged = NO;NS_ENDHANDLERif (NO == logged){ NSLog(@"Problem posting notification: %@", localException);} }NS_ENDHANDLER}}lockNCTable(TABLE);GSIArrayEmpty(a);unlockNCTable(TABLE);RELEASE(notification);
}
流程
简单的说就是查找到对应的根据通知的参数查找到对应的observation(不需要添加插入),然后按顺序存到链表中,找完之后按顺序遍历执行 ;
1.首先检查参数中的name是否存在 ,不存在报错 ;
2.首先把wildcard中可以接收所有的observation存入链表中 (查找所有未指定name或object的观察者,加在a数组中,即将wildcard表中的数据都加在新建链表中)
3.然后在nameless table中查找与参数object(先判断obect为nil的情况,然后在判断object为nil的情况,两种情况用于查找的key不同)对应的observation链表,把其中的元素也遍历插入执行链表中 (查找与通知的object相同但是没有name的观察者,加在a数组中)
4.最后在name table中查找,同理,先找name,然后再从中找对应object的obsevation链表,把其中的元素也遍历插入执行链表中 (先匹配name,首先,查找与通知的object相同的观察者,接着是没有object的观察者,都加在新建链表中)
5.最后遍历执行链表中的observation,给observer发送selector,让其处理
- 注意:关于能不能查找到的问题,我们只需要知道它是从表外往表里找的就行了,下面会提到一个例子 ;
移除通知
不给出源码了:
流程
1.若 NotificationName 和 object 都为 nil,则清空 wildcard 链表。
2.若 NotificationName 为 nil,遍历 named table,若 object 为 nil,则清空 named table,若 object 不为 nil,则以 object 为 key 找到对应的链表,然后清空链表。在 nameless table 中以 object 为 key 找到对应的 observer 链表,然后清空,若 object 也为 nil,则清空 nameless table。
3.若 NotificationName 不为nil,在 named table 中以 NotificationName 为 key 找到对应的 table,若 object 为 nil,则清空找到的 table,若 object 不为 nil,则以 object 为 key 在找到的 table 中取出对应的链表,然后清空链表。
一些问题
下面的方法不会接收到通知?
// 添加观察
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 通知发送
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
这里我理解的就两点:
- 注册是无论如何都可以注册的,而且也是从外往里注册的
- 通知发送(查找)是先从wildcard到nameless table到name table的顺序查找的,而且都是从外往里找的;这样也能知道通知执行的顺序 ;
- 或者是只要发送通知的参数和观察者的关系是前者包含后者,就可以找到
通知的发送时同步的,还是异步的?发送消息与接收消息的线程是同一个线程么?
通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。
而且要注意**接收通知的线程,和发送通知所处的线程是同一个线程。**也就是说如果要在接收通知的时候更新 UI,需要注意发送通知的线程是否为主线程。
如何使用异步发送通知?
1.让通知事件处理方法的内部实现再次说明在子线程实现 :
- (void)viewDidLoad {[super viewDidLoad];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {NSLog(@"--current thread: %@", [NSThread currentThread]);NSLog(@"Begin post notification");[[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];NSLog(@"End");
}//- (void)test {
// NSLog(@"--current thread: %@", [NSThread currentThread]);
// NSLog(@"Handle notification and sleep 3s");
// sleep(3);
//}- (void)test {dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);dispatch_async(queue, ^{ // 异步执行 + 串行队列NSLog(@"--current thread: %@", [NSThread currentThread]);NSLog(@"Handle notification and sleep 3s");sleep(3);});
}
2.可以通过 NSNotificationQueue 的 enqueueNotification: postingStyle: 和 enqueueNotification: postingStyle: coalesceMask: forModes: 方法将通告放入队列,实现异步发送,在把通告放入队列之后,这些方法会立即将控制权返回给调用对象。
这里的异步可能有点奇怪,我的理解是:这里的异步是指和发送通知这个任务异步,而不是队列中的通知异步发送 ;
比如:
- (void)viewDidLoad {[super viewDidLoad];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);dispatch_async(queue, ^{ // 异步执行 + 串行队列NSLog(@"--current thread: %@", [NSThread currentThread]);NSLog(@"Begin post notification");[[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];NSLog(@"End");});
}- (void)test {NSLog(@"--current thread: %@", [NSThread currentThread]);NSLog(@"Handle notification and sleep 3s");sleep(3);
}
这里[[NSNotificationCenter defaultCenter] postNotificationName:@“NotificationName” object:nil];在队列中执行时,会添加test的同步执行任务进队列,这样导致了要先等待test执行完毕才会解除阻塞完成[[NSNotificationCenter defaultCenter] postNotificationName:@“NotificationName” object:nil];的任务,最后才能执行NSLog(@“End”);
但如果使用通知队列:
- (void)viewDidLoad {[super viewDidLoad];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {NSLog(@"--current thread: %@", [NSThread currentThread]);NSLog(@"Begin post notification");NSNotification *notification = [NSNotification notificationWithName:@"NotificationName" object:nil];[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];NSLog(@"End");
}- (void)test {NSLog(@"--current thread: %@", [NSThread currentThread]);NSLog(@"Handle notification and sleep 3s");sleep(3);
}
这时执行[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];时,会把通知的任务加入通知队列,这时发送消息的任务已经完成了,于是就可以向主队列中同步添加NSLog(@“End”);,而通知队列中的通知时异步添加到串行队列队列中的,虽然没有创建新的线程,任务的执行也是顺序执行的,但这也意味着这里的通知执行没有阻塞发送消息的任务,所以这里的通知执行和发送消息的任务是异步的 ;
页面销毁时不移除通知会崩溃吗?
在观察者对象释放之前,需要调用 removeOberver 方法将观察者从通知中心移除,否则程序可能会出现崩溃。**(因为这个时候可能出现野指针)**但从 iOS9 开始,即使不移除观察者对象,程序也不会出现异常。
这是因为在 iOS9 以后,通知中心持有的观察者由 unsafe_unretained 引用变为weak引用。即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空。但是通过 addObserverForName:object: queue:usingBlock: 方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。
多次添加同一个通知会是什么结果?多次移除通知呢?
- 多次添加同一个通知,观察者方法会调用多次(可以看之前的源码,在注册观察者时不会对链表里面原来的判断,而是直接加入链表末尾,等到发送通知在表中查找观察者时,只要找到就会执行) ;
- 多次移除,没关系。
为什么注册通知时可以空名注册,但是发送通知时却不可以?
具体的原因不好说,但实际出现这种现象的原因是因为在发送通知的方法里面最开始就有判段是否空名,注册就没有 ;
object是干嘛的?是不是可以用来传值?
object是用来过滤Notification的,只接收指定的sender所发的Notification,传值请用userInfo,而不是object。