iOS进阶之NSNotification的实现原理

一、NSNotification使用

1、向观察者中心添加观察者:

  • 方式一:观察者接收到通知后执行任务的代码在发送通知的线程中执行
- (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

2、通知中心向观察者发送消息

- (void)postNotification:(NSNotification *)notification;

- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;

- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

3、移除观察者

- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

二、实现原理

1、首先了解Observation、NCTable这个结构体内部结构

当你调用addObserver:selector:name:object:会创建一个Observation,Observation的结构如下代码:

typedef struct  Obs {
  id observer;  //接受消息的对象   
  SEL selector; //执行的方法
  struct Obs    *next;  //下一Obs节点指针
  int   retained;   //引用计数
  struct NCTbl *link; //执向chunk table指针
} Observation;

对于Observation持有observer:

  • 在iOS9以前:

    • 持有的是一个__unsafe_unretain指针对象,当对象释放时,会访问已经释放的对象,造成BAD_ACCESS。
    • 在iOS9之后:持有的是weak类型指针,当observer释放时observer会置nil,nil对象performSelector不再会崩溃。
  • name和Observation是映射关系。

    • observer和sel包含在Observation结构体中。

Observation对象存在哪?

NSNotification维护了全局对象表NCTable结构,结构体里包含GSIMapTable表的结构,用于存储Observation。代码如下:

#define CHUNKSIZE   128
#define CACHESIZE   16
typedef struct NCTbl {
  Observation       *wildcard;  /* Get ALL messages.        */
  GSIMapTable       nameless;   /* Get messages for any name.   */
  GSIMapTable       named;      /* Getting named messages only. */
  unsigned      lockCount;  /* Count recursive operations.  */
  NSRecursiveLock   *_lock;     /* Lock out other threads.  */
  Observation       *freeList;
  Observation       **chunks;
  unsigned      numChunks;
  GSIMapTable       cache[CACHESIZE];
  unsigned short    chunkIndex;
  unsigned short    cacheIndex;
} NCTable;

数据结构重要的参数:

  • wildcard:保存既没有通知名称又没有传入object的通知单链表;
  • nameless:存储没有传入名字的通知名称的hash表。
  • named:存储传入了名字的通知的hash表。
  • cache:用于快速缓存.

这里值得注意nameless和named的结构,虽然都是hash表,存储的东西还有点区别:

  • nameless表中的GSIMapTable的结构如下
key value
object Observation
object Observation
object Observation

没有传入名字的nameless表,key就是object参数,vaule为Observation结构体

  • 在named表中GSIMapTable结构如下:
key value
name maptable
name maptable
name maptable
  • maptable也是一个hash表,结构如下:
key value
object Observation
object Observation
object Observation

传入名字的通知是存放在叫named的hash表
kay为name,value还是maptable也是一个hash表
maptable表的key为object参数,value为Observation参数

2、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;

    //入参检查异常处理
    ...
        //table加锁保持数据一致性,同一个线程按顺序执行,是同步的
    lockNCTable(TABLE);
        //创建Observation对象包装相应的调用函数
    o = obsNew(TABLE, selector, observer);
        //处理存在通知名称的情况
    if (name)
    {
        //table表中获取相应name的节点
        n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
        if (n == 0)
        {
           //未找到相应的节点,则创建内部GSIMapTable表,以name作为key添加到talbe中
          m = mapNew(TABLE);
          name = [name copyWithZone: NSDefaultMallocZone()];
          GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
          GS_CONSUMED(name)
        }
        else
        {
            //找到则直接获取相应的内部table
            m = (GSIMapTable)n->value.ptr;
        }

        //内部table表中获取相应object对象作为key的节点
        n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
        if (n == 0)
        {
            //不存在此节点,则直接添加observer对象到table中
            o->next = ENDOBS;//单链表observer末尾指向ENDOBS
            GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
        }
        else
        {
            //存在此节点,则获取并将obsever添加到单链表observer中
            list = (Observation*)n->value.ptr;
            o->next = list->next;
            list->next = o;
        }
    }
    //只有观察者对象情况
    else if (object)
    {
        //获取对应object的table
        n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
        if (n == 0)
        {
            //未找到对应object key的节点,则直接添加observergnustep-base-1.25.0
            o->next = ENDOBS;
            GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
        }
        else
        {
            //找到相应的节点则直接添加到链表中
            list = (Observation*)n->value.ptr;
            o->next = list->next;
            list->next = o;
        }
    }
    //处理即没有通知名称也没有观察者对象的情况
    else
    {
        //添加到单链表中
        o->next = WILDCARD;
        WILDCARD = o;
    }
        //解锁
    unlockNCTable(TABLE);
}

添加通知的基本逻辑:

  1. 根据传入的selector和observer创建Observation,并存入GSIMaptable中,如果已存在,则是从cache中取。

  2. 如果name存在:

    • 则向named表中插入元素,key为name,value为GSIMaptable。
    • GSIMaptable里面key为object,value为Observation,结束
  3. 如果name不存在:

    • 则向nameless表中插入元素,key为object,value为Observation,结束
  4. 如果name和object都不存在,则把这个Observation添加WILDCARD链表中

三、addObserverForName:object:queueusingBlock:实现原理

//对于block形式,里面创建了GSNotificationObserver对象,然后在调用addObserver: selector: name: object:
- (id) addObserverForName: (NSString *)name 
                   object: (id)object 
                    queue: (NSOperationQueue *)queue 
               usingBlock: (GSNotificationBlock)block
{
    GSNotificationObserver *observer = 
        [[GSNotificationObserver alloc] initWithQueue: queue block: block];

    [self addObserver: observer 
             selector: @selector(didReceiveNotification:) 
                 name: name 
               object: object];

    return observer;
}

/*
1.初始化该队列会创建Block_copy 拷贝block
2.并确定通知操作队列
*/
- (id) initWithQueue: (NSOperationQueue *)queue 
               block: (GSNotificationBlock)block
{
    self = [super init];
    if (self == nil)
        return nil;

    ASSIGN(_queue, queue);
    _block = Block_copy(block);
    return self;
}

/*
1.通知的接受处理函数didReceiveNotification,
2.如果queue不为空,通过addOperation来实现指定操作队列处理
3.如果queue不为空,直接在当前线程执行block。
*/
- (void) didReceiveNotification: (NSNotification *)notif
{
    if (_queue != nil)
    {
        GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc] 
            initWithNotification: notif block: _block];

        [_queue addOperation: op];
    }
    else
    {
        CALL_BLOCK(_block, notif);
    }
}

4、发送通知的实现 postNotificationName: name: object:

 - (void) _postAndRelease: (NSNotification*)notification
{
    1.入参检查校验
    2.创建存储所有匹配通知的数组GSIArray
    3.加锁table避免数据一致性问题
    4.查找既不监听name也不监听object所有的wildcard类型的Observation,加入数组GSIArray中
    5.查找NAMELESS表中指定对应观察者对象object的Observation并添加到数组中
    6.查找NAMED表中相应的Observation并添加到数组中
        1. 首先查找name与object的一致的Observation加入数组中
        2. 当object为nil的Observation加入数组中
    //解锁table
    //遍历整个数组并依次调用performSelector:withObject处理通知消息发送
    //解锁table并释放资源
}

二、NSNotification相关问题

1、对于addObserver方法,为什么需要object参数?

  1. addObserver当你不传入name也可以,传入object,当postNotification方法同样发出这个object时,就会触发通知方法。

因为当name不存在的时候,会继续判断object,则向nameless的maptable表中插入元素,key为object,value为Observation

2、都传入null对象会怎么样

你可能也注意到了,addObserver方法name和object都可以为空,这表示将会把observer赋值为 wildcard,他将会监听所有的通知。

3、通知的发送时同步的,还是异步的。

同步异步这个问题,由于TABLE资源的问题,同一个线程会按顺序遍历数组执行,自然是同步的。

4、NSNotificationCenter接受消息和发送消息是在一个线程里吗?如何异步发送消息

由于是使用的performSelector方法,没有进行转线程,默认是postNotification方法的线程。

[o->observer performSelector: o->selector 
withObject: notification];

对于异步发送消息,可以使用NSNotificationQueue,queue顾明意思,我们是需要将NSNotification放入queue中执行的。

NSNotificationQueue发送消息的三种模式:

typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // 当runloop处于空闲状态时post
    NSPostASAP = 2, // 当当前runloop完成之后立即post
    NSPostNow = 3  // 立即post
};

NSNotification *notification = [NSNotification notificationWithName:@"111" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification: notification postingStyle:NSPostASAP];
  • NSPostingStyle为NSPostNow 模式是同步发送,
  • NSPostWhenIdle或者NSPostASAP是异步发送

5、NSNotificationQueue和runloop的关系?

NSNotificationQueue 是依赖runloop才能成功触发通知,如果去掉runloop的方法,你会发现无法触发通知。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //子线程的runloop需要自己主动开启     
   NSNotification *notification = [NSNotification notificationWithName:@"TEST" object:nil];
        [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing forModes:@[NSDefaultRunLoopMode]];
        // run runloop
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSRunLoopCommonModes];
        CFRunLoopRun();
        NSLog(@"3");
    });

NSNotification *notification = [NSNotification notificationWithName:@"111" object:nil];
[[NSNotificationQueue defaultQueue] enqueueNotification: notification postingStyle:NSPostASAP];

NSNotificationQueue将通知添加到队列中时,其中postringStyle参数就是定义通知调用和runloop状态之间关系。

6、如何保证通知接收的线程在主线程?

  1. 保证主线程发送消息或者接受消息方法里切换到主线程

  2. 接收到通知后跳转到主线程,苹果建议使用NSMachPort进行消息转发到主线程。

实现代码如下:

machPort转发到主线程_1.png
machPort转发到主线程_2.png
machPort转发到主线程_3.png
machPort转发到主线程_4.png
machPort转发到主线程_5.png
  1. 使用block接口addObserverForName:object:queue:usingBlock:指定主线程

7、页面销毁时不移除通知会崩溃吗?

在iOS9之前会,iOS9之后不会

对于Observation持有observer

在iOS9之前:不是一个类似OC中的weak类型,持有的相当与一个__unsafe_unretain指针对象,当对象释放时,会访问已经释放的对象,造成BAD_ACCESS。
在iOS9之后:持有的是weak类型指针,对nil对象performSelector不再会崩溃

8、多次添加同一个通知会是什么结果?多次移除通知呢?

  1. 由于源码中并不会进行重复过滤,所以添加同一个通知,等于就是添加了2次,回调也会触发两次。

  2. 关于多次移除,并没有问题,因为会去map中查找,找到才会删除。当name和object都为nil时,会移除所有关于该observer的WILDCARD

9、下面的方式能接收到通知吗?为什么

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];

[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

根据postNotification的实现:

  • 会找到key为TestNotification的maptable,
  • 再从中选择key为nil的observation,
  • 所以是找不到以@1为key的observation的
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容