iOS底层系列31 -- Notification的底层原理

  • 关于NSNotification通知的源码下载地址点击这里
image.png
  • iOS中通知的使用步骤,主要分为两个步骤:
    • 第一步:在通知中心注册通知;
    • 第二步:通知中心调用post函数,发送通知,观察者接受到通知执行回调函数,实现如下:
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor redColor];
    //第一步:注册通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"YanZi" object:nil];
}

//接受到通知,执行回调
- (void)receiveNotification:(NSNotification *)notification {
    NSString *str = notification.userInfo[@"data"];
    NSLog(@"%@",str);
}

//第二步:发送通知
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"YanZi" object:nil userInfo:@{@"data":@"yanzi"}];
}
@end
注册通知部分
  • 调用addObserver:selector:name: object: 向通知中心NSNotificationCenter注册观察者,观察者接收到通知后执行任务的代码(selector)在 发送通知的线程 中执行
  • 调用addObserverForName:object: queue: usingBlock: 向通知中心NSNotificationCenter注册观察者,观察者接受到通知后执行任务的代码在 指定的操作队列 中执行
  • 上述两种方法,底层实现都会会创建一个Observation对象,Observation的结构体如下所示:
typedef struct  Obs {
  id        observer;   //接受消息的对象
  SEL       selector;    //回调方法
  struct Obs    *next;      //下一个Obs的节点指针
  int       retained;  //引用计数
  struct NCTbl  *link;      /* Pointer back to chunk table  */
} Observation;
  • Observation结构体内部有一个observer成员,即观察者也就是接受消息的对象;
  • addObserver:selector:name: object:的源码实现如下:
- (void)addObserver:(id)observer selector:(SEL)selector name:(NSString*)name object:(id)object {
  Observation   *list;
  Observation   *o;
  GSIMapTable   m;
  GSIMapNode    n;
 
  //入参的异常检测......
  
  //保证线程安全
  lockNCTable(TABLE);

  o = obsNew(TABLE, selector, observer);
  //通知名称存在时
  if (name){
      //NAMED是一个哈希表 根据name 取出节点node
      n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
      if (n == 0){
         //节点为空 创建maptable
         m = mapNew(TABLE);
         name = [name copyWithZone: NSDefaultMallocZone()];
         //将 maptable与name 以键值对的形式 存入NAMED哈希表中
         GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
         GS_CONSUMED(name)
      }else{
         m = (GSIMapTable)n->value.ptr;
      }
      //以object为key 在maptable哈希表中获取指定节点
      n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
      if (n == 0){
         o->next = ENDOBS;
         //若节点为空 将object与Observation 以键值对的形式 存入maptable哈希表中
         GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
      }else{
         //若节点存在 将Observer添加到Observation单链表中
         list = (Observation*)n->value.ptr;
         o->next = list->next;
         list->next = o;
      }
  }else if (object){
      //name为空 以object为key 从NAMELESS哈希表中取出 节点
      n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
      if (n == 0){
          o->next = ENDOBS;
          //节点不存在 以object与observation为键值对 存入NAMELESS哈希表中
          GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
      }else{
          //节点存在 将将Observer添加到Observation单链表中
          list = (Observation*)n->value.ptr;
          o->next = list->next;
          list->next = o;
      }
  }else{
      //当name与object都不存在的情况下 将Observation添加到WILDCARD单链表中
      o->next = WILDCARD;
      WILDCARD = o;
  }
  unlockNCTable(TABLE);
}
  • 在阐述执行逻辑之间,首先介绍observer对象是怎么进行存储的;
  • 首先有一个named哈希表,当传入的通知name有值时,observer观察者最终会存储这个named的哈希表中,其结构如下所示:
    image.png
  • 可以看到name通知名称,其与maptable组成键值对存储到named哈希表中,而maptable也是一个哈希表,其存储的是object与observation的键值对,observation可以看成是observer观察者;
  • 其次还有一个nameless哈希表,当传入的通知名称为空时,observer观察者最终会存储在nameless哈希表中,其结构如下:
    image.png
  • 添加通知观察者的基本逻辑如下:
  • 首先,根据入参selector和observer封装成一个Observation对象;
  • 其次判断通知名称name是否存在;
    • 若通知名称name存在时,首先将name与maptable以键值对的形式添加到named哈希表中,然后将Observation与object以键值对的形式添加到maptable哈希表中;
    • 若通知名称name不存在,object存在时,最终将Observation与object以键值对的形式添加到nameless哈希表中;
    • 若通知名称name不存在,object也不存在时,将Observation添加到WILDCARD链表中;
  • 上述的逻辑关系见下图所示:
image.png
发送通知部分
  • 调用postNotificationName: object:userInfo:方法,源码如下:
- (void)postNotificationName:(NSString*)name object:(id)object userInfo:(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;
  //...
  object = [notification object];
  GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);
  lockNCTable(TABLE);
  //当name与object均不存在时,遍历WILDCARD链表中的observation对象 添加到数组a中
  for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next){
      GSIArrayAddItem(a, (GSIArrayItem)o);
  }
  //当name不存在,但object存在时,遍历NAMELESS哈希表,将所有observation对象 添加到数组a中
  if (object){
      n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
      if (n != 0){
      o = purgeCollectedFromMapNode(NAMELESS, n);
      while (o != ENDOBS){
          GSIArrayAddItem(a, (GSIArrayItem)o);
          o = o->next;
      }
     }
   }
  //当name存在时 遍历NAMED哈希表,将所有observation对象 添加到数组a中
  if (name){
      n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
      if (n){
         m = (GSIMapTable)n->value.ptr;
      }else{
         m = 0;
      }
      if (m != 0){
         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){
          n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);
          if (n != 0){
              o = purgeCollectedFromMapNode(m, n);
              while (o != ENDOBS){
                GSIArrayAddItem(a, (GSIArrayItem)o);
                o = o->next;
            }
          }
        }
      }
  }
  unlockNCTable(TABLE);
  //遍历a数组 获取所有Observation中的observe对象 然后通过调用performSelector: 让观察者去调用selector方法(通知回调方法)
  count = GSIArrayCount(a);
  while (count-- > 0){
      o = GSIArrayItemAtIndex(a, count).ext;
      if (o->next != 0){
          NS_DURING{
              //观察者去调用selector方法(通知回调方法)
              [o->observer performSelector: o->selector withObject: notification];
          }
          //...
       }
  }
  lockNCTable(TABLE);
  GSIArrayEmpty(a);
  unlockNCTable(TABLE);
  RELEASE(notification);
}
  • WILDCARD链表中named哈希表中nameless哈希表中获取Observation对象存储到数组GSIArray中;
  • 然后遍历GSIArray数组,取出observer对象,执行selector通知回调方法;
面试题一:针对addObserver方法,当name为nil,object不为nil时,能否执行通知回调,若name与object都为nil时,发送通知时会发生什么?
  • 首先name为nil,object不为nil,Observation会被存储到nameless哈希表中,发送通知时会取出observer执行通知回调selector方法;
  • 其次name与object均为nil时,Observation会被存储到wildcard链表中,它会监听所有通知的回调
面试题二:NSNotification发送是同步的还是异步的?如何实现异步发送通知?
  • 所谓通知的同步是指:通知中心发送通知后 需要等待观察者处理完成消息后 再继续执行下面的逻辑;
  • NSNotification发送默认是同步的,代码实现如下:
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //第一步:注册通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"111" object:nil];
}

//接受到通知,执行回调
- (void)receiveNotification:(NSNotification *)notification {
    NSLog(@"%@",[NSThread currentThread]);
    NSLog(@"收到通知");
    sleep(3);
}

//第二步:发送通知
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"111" object:nil userInfo:@{@"data":@"发送通知"}];
    NSLog(@"通知发送完毕");
}
@end
  • 控制台执行结果如下:
image.png
  • 实现异步发送通知,方式一:让通知回调方法在子线程中执行,实现如下:
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //第一步:注册通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"111" object:nil];
}

//接受到通知,执行回调
- (void)receiveNotification:(NSNotification *)notification {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"%@",[NSThread currentThread]);
        NSLog(@"收到通知");
        sleep(3);
    });
}

//第二步:发送通知
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [[NSNotificationCenter defaultCenter] postNotificationName:@"111" object:nil userInfo:@{@"data":@"发送通知"}];
    NSLog(@"通知发送完毕");
    NSLog(@"%@",[NSThread currentThread]);
}
@end
  • 控制台调试结果如下:
image.png
  • 实现异步发送通知,方式二:可以通过NSNotificationQueueenqueueNotification: postingStyle:enqueueNotification: postingStyle: coalesceMask: forModes:方法,将通知放入队列,实现异步发送,在把通告放入队列之后,这些方法会立即将控制权返回给调用对象,实现如下:
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //第一步:注册通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"111" object:nil];
}

//接受到通知,执行回调
- (void)receiveNotification:(NSNotification *)notification {
    NSLog(@"%@",[NSThread currentThread]);
    NSLog(@"收到通知");
    sleep(3);
}

//第二步:发送通知
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSNotification *notification = [NSNotification notificationWithName:@"111" object:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle];
    NSLog(@"通知发送完毕");
    NSLog(@"%@",[NSThread currentThread]);
}
@end
  • 控制台调试结果如下:
image.png
面试题三:NSNotificationQueue与RunLoop之间的关系?
  • NSNotificationQueue需依赖RunLoop才能成功触发通知 若子线程中不创建RunLoop是无法触发通知回调的,代码实现如下:
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //第一步:注册通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveNotification:) name:@"111" object:nil];
}

//接受到通知,执行回调
- (void)receiveNotification:(NSNotification *)notification {
    NSLog(@"%@",[NSThread currentThread]);
    NSLog(@"收到通知");
    sleep(3);
}

//第二步:发送通知
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //NSNotificationQueue依赖RunLoop才能成功触发通知 否则接收不到回调
    //子线程的runLoop需主动获取
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSNotification *notification = [NSNotification notificationWithName:@"111" object:nil];
        [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle];
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    });
}
@end
面试题四:页面销毁时不移除通知会崩溃么?多次添加同一个通知会怎样?多次移除同一个通知会怎样?
  • 页面销毁时不移除通知,在iOS9之前会导致崩溃,在iOS9之后不会导致崩溃,weak指针;
  • 多次添加同一个通知,由于底层源码未作过滤处理,那么发送通知时会触发多次回调;
  • 多次移除同一个通知,不会有什么影响;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容