IOS基础原理:Notifications

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、使用
    • 1、提供的属性和方法
    • 2、队列的合并策略和发送时机
    • 3、注意点
  • 二、注册通知源码解析
    • 1、存储容器
    • 2、解析注册通知方法
    • 3、判断是否是同一个通知的3种情况
  • 三、发送通知与删除通知源码解析
    • 1、发送通知源码解析
    • 2、删除通知源码解析
  • 四、异步通知
    • 1、NSNotificationQueue的异步发送
    • 2、把要发送的通知添加到队列,等待发送
    • 3、发送通知
    • 4、主线程响应通知
  • Demo
  • 参考文献

一、使用

1、提供的属性和方法

NSNotification
- (NSString*) name; // 通知的name
- (id) object; // 携带的对象
- (NSDictionary*) userInfo; // 配置信息
NSNotificationCenter
// 添加通知
- (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;
NSNotificationQueue
// 把通知添加到队列中,NSPostingStyle是个枚举
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;

// 删除通知,把满足合并条件的通知从队列中删除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;

用于异步发送消息的通知队列,这个异步并不是开启线程,而是把通知存到双向链表实现的队列里面,等待某个时机触发。触发时调用NSNotificationCenter的发送接口进行发送通知,这么看NSNotificationQueue最终还是调用NSNotificationCenter进行消息的分发,另外NSNotificationQueue是依赖runloop的,所以如果线程的runloop未开启则无效。


2、队列的合并策略和发送时机

把通知添加到队列等待发送,同时提供了一些附加条件供开发者选择,如:什么时候发送通知、如何合并通知等,系统给了如下定义。

表示通知的发送时机
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空闲时发送通知
    NSPostASAP = 2, // 尽快发送,这种情况稍微复杂,这种时机是穿插在每次事件完成期间来做的
    NSPostNow = 3 // 立刻发送或者合并通知完成之后发送
};
通知合并的策略,有些时候同名通知只想存在一个,这时候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0, // 默认不合并
    NSNotificationCoalescingOnName = 1, // 只要name相同,就认为是相同通知
    NSNotificationCoalescingOnSender = 2  // object相同
};

3、注意点

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

可以,因为notificationcenter对观察者的引用是weak,当观察者释放的时候,观察者的指针值被置为nil

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

会调用多次observeraction。多次移除没有任何影响。


二、注册通知源码解析

1、存储容器

NCTable是根容器,由NSNotificationCenter持有

NCTable结构体中核心的三个变量:wildcardnamednameless,在源码中直接用宏定义表示了:WILDCARDNAMELESSNAMED

typedef struct NCTbl
{
    // 链表结构,保存既没有name也没有object的通知
    Observation *wildcard;
    
    // 存储没有name但是有object的通知
    GSIMapTable nameless;
    
    // 存储带有name的通知,不管有没有object
    GSIMapTable named;
    ...
} NCTable;
Observation是存储观察者和响应方法的结构体
typedef    struct    Obs
{
    id        observer;// 观察者,接收通知的对象
    SEL        selector;// 响应方法
    struct Obs    *next;// 链表中的下一个Observation
    ...
} Observation;

2、解析注册通知方法

  • 判定是不是同一个通知要从nameobject区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都以此为基础。
  • 存储过程并没有做去重操作,这也解释了为什么同一个通知注册多次则响应多次
a、提供给外界使用的注册通知方法
  • observer:观察者,即通知的接收者
  • selector:接收到通知时的响应方法
  • name:通知name
  • object:携带对象
- (void) addObserver: (id)observer selector: (SEL)selector name: (NSString*)name object: (id)object
{
    // 前置条件判断
    ...
    
    // 创建一个observation对象,持有观察者和SEL,下面进行的所有逻辑就是为了存储它
    o = obsNew(TABLE, selector, observer);
    
    ...
}

b、情况一:如果name存在
if (name) {...}

NAMED是个宏,表示名为named的字典。如果通知的name存在,则以namekeynamed字典中取出值n(这个n其实被MapNode包装了一层,便于理解这里直接认为没有包装),这个n还是个字典。

n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);

n不存在,则先取缓存,如果缓存没有则新建一个map

if (n == 0)
{
    m = mapNew(TABLE);
    GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
    ...
}

n存在则把值取出来赋值给m

else
{
    m = (GSIMapTable)n->value.ptr;
}

❹ 然后以objectkey,从字典中取出对应的值,这个值就是Observation类型的链表,然后把刚开始创建的Observation对象o存储进去。

n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
if (n == 0)
{// 不存在,则创建
    o->next = ENDOBS;
    GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
}
else
{
    list = (Observation*)n->value.ptr;
    o->next = list->next;
    list->next = o;
}

c、情况二:如果name为空,但object不为空
else if (object)
{
    ...
}

❶ 以objectkey,从nameless字典中取出对应的valuevalue是个链表结构。

n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);

❷ 不存在则新建链表,并存到map

if (n == 0)
{
    o->next = ENDOBS;
    GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
}

❸ 存在则把值接到链表的节点上

else
{ 
    ...
}

d、情况三:name 和 object 都为空则存储到wildcard链表中
else
{
    o->next = WILDCARD;
    WILDCARD = o;
}

3、判断是否是同一个通知的3种情况

情况一:存在name(无论object是否存在)

如果注册通知时传入name,那么会是一个双层的存储结构。首先找到NCTable中的named表,这个表存储了name的通知。接着以name作为key,找到value,这个value依然是一个map。最后,map的结构是以object作为keyObservation对象为value,这个Observation对象的结构上面已经解释,主要存储了observer & SEL

情况二:只存在object

objectkey,从nameless字典中取出value,此value是个Observation类型的链表。接着把创建的Observation类型的对象o存储到链表中。只存在object时存储只有一层,那就是objectObservation对象之间的映射。

情况三:没有name和object

这种情况直接把Observation对象存放在了Observation *wildcard 链表结构中。


三、发送通知与删除通知源码解析

1、发送通知源码解析

发送通知的核心逻辑比较简单,基本上就是查找和调用响应方法,从三个存储容器中:namednamelesswildcard去查找对应的Observation对象,然后通过performSelector:逐一调用响应方法,这就完成了发送流程。

a、解析发送通知方法
- (void)postNotificationName: (NSString*)name object: (id)object userInfo: (NSDictionary*)info
{
    ...
}
❶ 构造一个GSNotification对象, GSNotification继承了NSNotification
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];

b、发送通知的核心函数

主要做了三件事:查找通知、发送、释放资源。

- (void)_postAndRelease: (NSNotification*)notification
{
    ...
}

❶ 通过name & objectnamednamelesswildcard表中查找对应的通知(保存了observersel)。

...

❷ 执行发送,即调用performSelector执行响应方法,从这里可以看出是同步的。

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

❸ 释放notification对象。

RELEASE(notification);

2、删除通知源码解析

因为查找时做了这个链表的遍历,所以删除时会把重复的通知全都删除掉

- (void)removeObserver: (id)observer
{
    if (observer == nil) return;
    
  [self removeObserver: observer name: nil object: nil];
}

查找时仍然以nameobject为准,再加上observer做区分。

- (void)removeObserver: (id)observer name: (NSString*)name object: (id)object
{
    if (name == nil && object == nil && observer == nil)
        return;
    ...
}

四、异步通知

1、NSNotificationQueue的异步发送

上面介绍的NSNotificationCenter都是同步发送的,接受消息和发送消息是在一个线程里。这里介绍关于NSNotificationQueue的异步发送,通过NSNotificationQueue将通知添加到队列当中,立即将控制权返回给调用者,在合适的时机发送通知,从而不会阻塞当前的调用。从线程的角度看并不是真正的异步发送,或可称为延时发送,它是利用了runloop的时机来触发的,所以如果在其他子线程使用NSNotificationQueue,需要开启runloop。由于最终还是通过NSNotificationCenter进行发送通知,所以从这个角度讲它还是同步的。所谓异步,指的是非实时发送而是在合适的时机发送,并没有开启异步线程。


2、把要发送的通知添加到队列,等待发送

NSPostingStylecoalesceMask在上面的类结构中有介绍。modes这个就和runloop有关了,指的是runloopmode

- (void) enqueueNotification: (NSNotification*)notification
        postingStyle: (NSPostingStyle)postingStyle
        coalesceMask: (NSUInteger)coalesceMask
            forModes: (NSArray*)modes
{
    ...
}

❶ 根据coalesceMask参数判断是否合并通知

if (coalesceMask != NSNotificationNoCoalescing)
{
    [self dequeueNotificationsMatching: notification
                          coalesceMask: coalesceMask];
}

❷ 接着根据postingStyle参数,判断通知发送的时机

switch (postingStyle)
{
    ...
}

runloop立即回调通知方法,同步发送

case NSPostNow:
{
    // 如果是立马发送,则调用NSNotificationCenter进行发送
    [_center postNotification: notification];
}

runloop在执行timer事件或sources事件的时候回调通知方法,异步发送

case NSPostASAP:
    // 添加到_asapQueue队列,等待发送
    add_to_queue(_asapQueue, notification, modes, _zone);

runloop空闲的时候回调通知方法,异步发送

case NSPostWhenIdle:
    // 添加到_idleQueue队列,等待发送
    add_to_queue(_idleQueue, notification, modes, _zone);

3、发送通知

runloop触发某个时机,调用GSPrivateNotifyASAP()GSPrivateNotifyIdle()方法,这两个方法最终都调用了notify()方法。notify()所做的事情就是调用NSNotificationCenterpostNotification:进行发送通知。

a、发送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_asapQueue,
        mode,
        item->queue->_zone);
}
b、发送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_idleQueue,
        mode,
        item->queue->_zone);
}
c、循环遍历发送通知
static void notify(NSNotificationCenter *center,
                   NSNotificationQueueList *list,
                   NSString *mode, NSZone *zone)
{
    for (pos = 0; pos < len; pos++)
    {
      NSNotification *n = (NSNotification*)ptr[pos];

      [center postNotification: n];
      RELEASE(n);
    }
}

4、主线程响应通知

异步线程发送通知则响应函数也是在异步线程,如果执行UI刷新相关的话就会出现问题,那么如何保证在主线程响应通知呢?可以使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block


Demo

Demo在我的Github上,欢迎下载。
BasicsDemo

参考文献

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

推荐阅读更多精彩内容