【iOS】通知传值实现

文章目录

概要

  • 通知传值可以跨越多个界面进行传值,支持多个接受者,多个对象可以同时接收一个通知并进处理,实现一对多通信。

实现流程如下:

发送通知:

objc 复制代码
[[NSNotificationCenter defaultCenter] postNotificationName:@"note" 
object:nil userInfo:@{@"name": self.label.text}];

​ 参数一:NSString类型,用于标识通知的唯一性,发送和接收通知的时候这个字段要相同

​ 参数二:发送通知的对象,可以为nil,主要用于区分通知的来源,如果只关心某个特定对象发出的通知,可·· 以指定object,反之传nil就行,

​ 参数三:通知携带的附加信息,字典类型。用于传递额外数据

接收通知:

objc 复制代码
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificate:) name:@"note" object:nil];

​ 参数一:监听者对象,收到通知之后会回调selector方法。

​ 参数二:SEL类型,回调指定的方法。

​ 参数三:通知的名字。

​ 参数四:直接收由该对象发送的通知,如果传为nil,表示接收所有对象发送的该名字的通知,如果指定了object则只接收这个对象发送的通知。

移除监听者:

objc 复制代码
[[NSNotificationCenter defaultCenter] removeObserver:self];

通知机制的关键类

NSNotification

首先是一个NSNotification,这是一个用于描述通知的类,一个通知对象包含下面几个属性:

objc 复制代码
@interface NSNotification : NSObject <NSCopying, NSCoding>
...
/* Querying a Notification Object */
- (NSString*) name; // 通知的名字
- (id) object; // 携带的对象
- (NSDictionary*) userInfo; // 额外信息

@end

其次是一个NSNotificationCenter,通知的核心就是一个与线程关联的对象即通知中心,通知中心发送通知给观察者是同步的,也可以使用NSNotificationQueue队列实现异步发送通知。(延迟到当前线程 RunLoop 的某个阶段发送)这个队列还可以实现一个合并通知的效果,如果短时间内连续入队多个同名通知,最终可能只发送一次,适用于频繁变化的场景。

objc 复制代码
static NSNotificationCenter *default_center = nil;

+ (NSNotificationCenter*) defaultCenter
{
  return default_center;
}

这里主要有三种放松时机:

objc 复制代码
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostNow];//表示立即发送

[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];//尽快发送,会等待当前调用栈结束,runloop有机会时再发送

[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostWhenIdle];//等待runloop空闲时再发送,适合一些不紧急的通知,如UI更新、状态刷新之类的

NSNotification依赖于runloop,所以如果当前线程没有正常运行runloop的时候,通知可能不会按照预期发送。(使用GCD的时候需要注意,子线程中也需要注意)

我们也可以指定在某种RunLoop Mode下发送,如果传nil表示默认模式。

objc 复制代码
[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP coalesceMask:NSNotificationCoalescingOnName forModes:@[NSDefaultRunLoopMode]];

NSNotificationCenter类主要负责三件事:

  • 添加通知
  • 发送通知
  • 移除通知
objc 复制代码
// 添加通知
- (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;

通知中心定义了两个结构体存储通知信息和观察者信息:

objc 复制代码
// 管理所有的观察者节点
typedef struct NCTbl {
    Observation *wildcard;;//存储所有的通知的观察者的一个链表
    GSIMapTable nameless;//存储没有执行通知名字,但是指定了object的观察者表
    GSIMapTable named;//存储指定了通知名的观察者表
    //...
} NCTable;

// 存储观察者和响应结构体,是基本的存储单元,保存一次完整的注册关系
typedef struct Obs {
    id observer;
    id receiver;
    SEL selector;
    BOOL owner;
    int32_t posting;
    struct Obs *next;
    struct NCTbl *link;//反向指向通知中心的内部表
} Observation;

我们先简单看一个查找流程:

objc 复制代码
post notification
      ↓
取出 notification.name / notification.object
      ↓
查 wildcard 链表
      ↓
查 nameless[object]
      ↓
查 named[name][object]
      ↓
查 named[name][nil]
      ↓
得到 Observation 数组
      ↓
依次调用 observer 的 selector

name表

named表是一个二级Map,大概结构如下:

objc 复制代码
named = {
    notificationName : {
        object : Observation链表
    }
}

第一层的key是通知的名字,value是objectMap

第二层的key是发送者object,value是一个Observation链表

内层使用链表结构实现保存多个观察者的情况。

在实际开发中,object参数经常传nil,系统会自动为根据这个nil生成一个key。

nameless表

由于没有通知名,因此就没了第一层的约束,只有object和observation的结构了。

wildcard表

这个表没有通知名也没有object,所以直接退化为了一个链表,存储了可以接收所有通知的类的信息。

NSNotificationQueue

objc 复制代码
NSNotification *notification = [NSNotification notificationWithName:@"DataDidChange" object:self userInfo:nil];

[[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];

通知队列,主要用于异步发送消息,这里需要注意的是,这个异步不是开启了一个新线程,而是将通知存储到双向链表实现的队列中,等待runloop到合适的时机进行调用,最终还是通过调用NSNotificationCenter进行消息的分法

NSNotification主要做的事由两件:

objc 复制代码
// 把通知添加到队列中
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 删除通知,将满足合并条件的通知从队列中删除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;

几个枚举参数结构如下:

objc 复制代码
// 表示通知的发送时机
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相同
};

同步通知机制代码

注册通知

这段代码的核心任务是把observer+selector这一组信息,按照name / object的匹配条件存储进通知中心内部的表结构中。

objc 复制代码
- (void)addObserver:(id)observer selector:(SEL)selector name:(NSString *)name object:(id)object
{
    Observation *list;//已有观察者链表的头节点
    Observation *o;
    GSIMapTable m;
    GSIMapNode n;

    if (observer == nil)//观察者不能为空
        [NSException raise:NSInvalidArgumentException
                    format:@"Nil observer passed to addObserver ..."];

    if (selector == 0)//没有指定回调方法
        [NSException raise:NSInvalidArgumentException
                    format:@"Null selector passed to addObserver ..."];

    if ([observer respondsToSelector:selector] == NO)//观察者没法响应selector
    {
        [NSException raise:NSInvalidArgumentException
                    format:@"[%@-%@] Observer '%@' does not respond to selector '%@'",
                           NSStringFromClass([self class]), NSStringFromSelector(_cmd),
                           observer, NSStringFromSelector(selector)];
    }

    lockNCTable(TABLE);//加锁,避免链表损坏等

    o = obsNew(TABLE, selector, observer);//创建通知中心真正的观察者节点

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

        if (n == 0)
        {
            m = mapNew(TABLE);
            GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void *)m);
            ...
        }
        else
        {
            m = (GSIMapTable)n->value.ptr;
        }

        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;
        }
    }
    else if (object)
    {
        n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);

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

    unlockNCTable(TABLE);
}

发送通知

将传入的通知参数封装成一个NSNotification/GSNotification对象,调用内部方法去真正发送通知。

objc 复制代码
// 发送通知
- (void) postNotification: (NSNotification*)notification {
  if (notification == nil) {
      [NSException raise: NSInvalidArgumentException
                  format: @"Tried to post a nil notification."];
    }
  [self _postAndRelease: RETAIN(notification)];
}

- (void) postNotificationName: (NSString*)name object: (id)object {
  [self postNotificationName: name object: object userInfo: nil];
}

- (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];
}

通过上面代码,可以看到最终都是进入了一个_postAndRelease:核心作用可以概括为:++根据通知的name和object找到所有符合条件的观察者,然后逐个调用观察者注册时指定的selector,最后释放notification++。

objc 复制代码
- (void) _postAndRelease: (NSNotification*)notification {
    Observation        *o;//一个观察者记录
    unsigned        count;//临时数组中观察者数量
    NSString        *name = [notification name];
    id                object;//保存通知携带的object
    GSIMapNode        n;
    GSIMapTable        m;
    GSIArrayItem        i[64];
    GSIArray_t        b;
    GSIArray        a = &b;
    
    if (name == nil) {//发送通知时,通知名不能为nil(注册观察者时可以不指定通知的名称)
        RELEASE(notification);
        [NSException raise: NSInvalidArgumentException
                    format: @"Tried to post a notification with no name."];
    }
    object = [notification object];//取出通知的object

    GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);//创建一个临时数组啊,用于保存本次通知要发送给哪些观察者,如果观察者数量不超过64可以直接使用栈上的空间,避免堆分配,提高性能
    lockNCTable(TABLE);
    
  //完全通配观察者,即object、name都为nil。purge方法会清理掉无效的节点
    for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next) {
        GSIArrayAddItem(a, (GSIArrayItem)o);
    }
    
  //处理没有name的情况
    if (object) {
        n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);//得到object对应的observation链表
        if (n != 0) {//找到了
            o = purgeCollectedFromMapNode(NAMELESS, n);//清理其中已经失效的观察者
            while (o != ENDOBS) {//遍历加入观察者数组
                GSIArrayAddItem(a, (GSIArrayItem)o);
                o = o->next;
            }
        }
    }

    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);

  //发送通知
    count = GSIArrayCount(a);
    while (count-- > 0) {
        o = GSIArrayItemAtIndex(a, count).ext;
        if (o->next != 0) {
            NS_DURING {
                [o->observer performSelector: o->selector
                                  withObject: notification];
            }
            NS_HANDLER {
                BOOL        logged;
                NS_DURING
                    NSLog(@"Problem posting %@: %@", notification, localException);
                    logged = YES;
                NS_HANDLER
                    logged = NO;
                NS_ENDHANDLER
                if (NO == logged) { 
                    NSLog(@"Problem posting notification: %@", localException);
                }  
            }
            NS_ENDHANDLER
        }
    }
    lockNCTable(TABLE);
    GSIArrayEmpty(a);
    unlockNCTable(TABLE);

    RELEASE(notification);
}

我们使用自然语言分析一下这段代码:

整体的流程可以分为:查找通知、发送、释放资源三个流程

  • 查找通知:通配观察者 -> 无name但是object匹配 -> name匹配object随便
  • 发送通知:从观察者数组中取出observer节点,通过performSelector:逐一调用sel
  • 释放资源:释放notification对象

删除通知

通过调用removeObserver方法实现移除通知:

objc 复制代码
- (void) removeObserver: (id)observer {
  if (observer == nil)
    return;

  [self removeObserver: observer name: nil object: nil];
}

- (void) removeObserver: (id)observer name: (NSString*)name object: (id)object {
  // 当要移除的信息都为空时,直接返回
  if (name == nil && object == nil && observer == nil)
      return;

  lockNCTable(TABLE);
  //在wildcard链表里删除对应observer的注册信息,然后返回新的链表头
  if (name == nil && object == nil) {
      WILDCARD = listPurge(WILDCARD, observer);
    }
  //不限制通知名,这里包含两种情况,可能覆盖所有的name,需要根据ojbect进行区分
  if (name == nil) {
      GSIMapEnumerator_t        e0;//枚举器
      GSIMapNode                n0;//当前遍历到的map节点
      // 首先尝试删除为此object对应的所有命名项目
      // 在named表中
      e0 = GSIMapEnumeratorForMap(NAMED);
      n0 = GSIMapEnumeratorNextNode(&e0);
      while (n0 != 0) {
          GSIMapTable m = (GSIMapTable)n0->value.ptr;
          NSString *thisName = (NSString*)n0->key.obj;

          n0 = GSIMapEnumeratorNextNode(&e0);
          if (object == nil) { // 如果object为空即清理某个observer在所有object下的注册,直接清除named表
          	  // 清空named表
              GSIMapEnumerator_t        e1 = GSIMapEnumeratorForMap(m);
              GSIMapNode                n1 = GSIMapEnumeratorNextNode(&e1);

              while (n1 != 0) {
                  GSIMapNode        next = GSIMapEnumeratorNextNode(&e1);

                  purgeMapNode(m, n1, observer);
                  n1 = next;
                }
          } else {
          // 以object为key找到对应链表,清空该链表
              GSIMapNode        n1;
              n1 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
              if (n1 != 0) {
                  purgeMapNode(m, n1, observer);
                }
            }
          if (m->nodeCount == 0) {
              mapFree(TABLE, m);
              GSIMapRemoveKey(NAMED, (GSIMapKey)(id)thisName);
            }
        }
      // 开始操作nameless表
      if (object == nil) { // object为空时
      	  // 清空nameless表
          e0 = GSIMapEnumeratorForMap(NAMELESS);
          n0 = GSIMapEnumeratorNextNode(&e0);
          while (n0 != 0) {
              GSIMapNode        next = GSIMapEnumeratorNextNode(&e0);

              purgeMapNode(NAMELESS, n0, observer);
              n0 = next;
            }
        } else { // object不为空
          // 找到对应的observer链表,清空该链表
          n0 = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
          if (n0 != 0) {
              purgeMapNode(NAMELESS, n0, observer);
            }
        }
   } else { // name不为空
      GSIMapTable                m;
      GSIMapEnumerator_t        e0;
      GSIMapNode                n0;

      n0 = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
      // 如果没有和这个name相同的key,直接返回
      if (n0 == 0) {
          unlockNCTable(TABLE);
          return;                /* Nothing to do.        */
      }
      m = (GSIMapTable)n0->value.ptr; // 找到name作为key对应的数据信息

      if (object == nil) {
      // 如果object为nil,就清空刚才找到的name对应的数据信息
          e0 = GSIMapEnumeratorForMap(m);
          n0 = GSIMapEnumeratorNextNode(&e0);

          while (n0 != 0) {
              GSIMapNode        next = GSIMapEnumeratorNextNode(&e0);

              purgeMapNode(m, n0, observer);
              n0 = next;
            }
      } else {
      // 如果object不为空,清空object对应的链表
          n0 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
          if (n0 != 0) {
              purgeMapNode(m, n0, observer);
            }
        }
      // 因为其中的数据清除完了,所以还需要清除named表中的作为key的name
      if (m->nodeCount == 0) {
          mapFree(TABLE, m);
          GSIMapRemoveKey(NAMED, (GSIMapKey)((id)name));
        }
    }
  unlockNCTable(TABLE);
}

我们同样使用自然语言概述如下:

  • 如果name和object都为nil:只需要在wildcard链表中移除observer
  • 如果name为nil:object有两种情况
    • named表:遍历所有name,找到每个name下的object,如果object为空,就直接清空named表,反之只清空指定object的。如果某个name下所有的object都被清空了,就直接讲这个name从named表中移除
    • nameless表:如果object为nil,就清空nameless表中所有的observer。如果不为空,只清理object对应的observer
  • 如果name不为空:
    • 首先在named表中找到name对应的map,如果object为nil,就清空改name下所有的observer。如果object不为nil,就只清空name下object对应的observer,如果name下所有的observer都被清空,需要讲name从named表中移除。

异步通知实现机制

如前面所介绍的,异步通知机制是使用NSNotificationQueue实现的,但是并没有真正创建子线程,只是利用率NSRunLoop实现的一个延迟发送。

入队

objc 复制代码
// 定义通知发送时机的枚举
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相同,则认为通知重复,合并处理
};

- (void)enqueueNotification:(NSNotification *)notification
                postingStyle:(NSPostingStyle)postingStyle
                coalesceMask:(NSUInteger)coalesceMask
                    forModes:(NSArray *)modes
{
    // 如果设置了合并策略,先移除已有队列中与当前通知重复的通知
    if (coalesceMask != NSNotificationNoCoalescing) {
        // dequeueNotificationsMatching:coalesceMask: 方法会根据策略
        // 删除name或object重复的通知,确保队列中不会重复发送
        [self dequeueNotificationsMatching:notification
                               coalesceMask:coalesceMask];
    }

    // 根据发送策略选择队列
    switch (postingStyle) {
        case NSPostNow: {
            // 立即发送通知,不加入队列
            // 如果当前正在处理通知队列,也可能会合并其他待发送通知
            [_center postNotification:notification];
            break;
        }

        case NSPostASAP: {
            // 尽快发送,加入_asapQueue队列
            // _asapQueue会在事件处理完成后尽快被处理
            // modes指定通知在哪些runloop mode下可以被触发
            add_to_queue(_asapQueue, notification, modes, _zone);
            break;
        }

        case NSPostWhenIdle: {
            // 空闲时发送,加入_idleQueue队列
            // _idleQueue通常在runloop空闲时才会处理
            add_to_queue(_idleQueue, notification, modes, _zone);
            break;
        }

        default:
            // 未知postingStyle,通常不会走到这里
            break;
    }
}

发送通知

objc 复制代码
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);
	}
	......	
}
// 发送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode) {
	notify(item->queue->_center,
	    item->queue->_asapQueue,
	    mode,
	    item->queue->_zone);
}
// 发送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode) {
    notify(item->queue->_center,
    	item->queue->_idleQueue,
    	mode,
    	item->queue->_zone);
}

runloop会在合适的时机触发,调用GSPrivateNotifyASAP()GSPrivateNotifyIdle()方法,两个方法最终都调用notify()方法

主线程响应通知

如果异步线程发送通知则相应函数也是在异步线程,如果此时执行UI刷新的话就会出现问题。对此我们有两种情况解决这个问题:

  1. 使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block

  2. 利用前面学习的runloop线程保活,在主线程注册machPort,通过线程通信,在异步线程收到消息的时候给machPort发送消息

在iOS9开始,页面销毁时不移除通知并不会崩溃,这是因为通知中心持有的观测者的引用由unsafe_unretained变为weak,持有观测者的引用会在观测者被回收后自动置空。但是通过addObserverForName:object: queue:usingBlock: 方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。

相关推荐
2601_955767421 小时前
iPhone 17 屏幕偏振光学解析与保护贴技术选择——悟赫德观复盾护景贴
人工智能·科技·ios·ar·iphone·圆偏振光
起个破名想半天了1 小时前
VMWare安装macOS虚拟机教程
macos
qq3621967052 小时前
手机App下载安装完全指南:2026最新教程(Android & iOS)
android·ios·智能手机
核电机组3 小时前
IOS原生APP集成Flutter
flutter·ios
亘元有量-流量变现3 小时前
预算有限怎么投?iOS ASO与ASA优劣对比及精细化联动增长策略
ios·用户运营·aso优化·亘元有量·方糖试玩
科技侃谈4 小时前
Windows和Mac日常使用推荐?联想小新 Air 15 给出大屏新答案
macos
PersistJiao4 小时前
针对 Intel Mac 的硬件限制提升faster-whisper字幕识别的方法
macos·whisper
Mr -老鬼5 小时前
EasyClick iOS自动化7.1.0新版本详解
ios·自动化·easyclick
2501_916007475 小时前
不用 Mac 也可以 Windows下管理iOS描述文件的非Xcode完整指南
android·ios·小程序·https·uni-app·iphone·webview