【iOS底层分析】消息通知机制

2,024 阅读8分钟

iOS常见的消息通知机制有如下三种,delegate不多赘述,本篇主要探究KVONSNotification的底层原理实现及对比。

源码地址:gnu/lib-base

方式delegateKVONSNotificationCenter
通知方式一对一一对多一对多
适用语言OC+SwiftOC+继承自NSObject的Swift类对象OC+Swift
描述可以有返回值互相的引用关系协同工作 处理业务逻辑只能作用在value以及变化上需要相互依赖单方向无协同能携带更多的信息常用于系统级消息无依赖单方向无协同

KVO

基础

  • KVO键值观察是一种机制,允许将其他对象的指定属性更改通知给观察者对象

  • 基于KVC

You can only use key-value observing with classes that inherit from NSObject.Mark properties that you want to observe through key-value observing with both the @objc attribute and the @dynamic modifier.(你只能对继承自NSObject的类使用KVO。用@objc属性和@dynamic修饰符标记您想要通过键值观察来观察的属性)

 

KVO基本用法如下


// 添加观察者

- (void)addObserver:(NSObject *)observer

         forKeyPath:(NSString *)keyPath

            options:(NSKeyValueObservingOptions)options

            context:(nullable void *)context;

  


// 实现KVO回调

- (void)observeValueForKeyPath:(NSString *)keyPath

                      ofObject:(id)object

                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change

                       context:(void *)context

  


// 移除观察者

- (void)removeObserver:(NSObject *)observer

            forKeyPath:(NSString *)keyPath

               context:(nullable void *)context;

- (void)removeObserver:(NSObject *)observer

            forKeyPath:(NSString *)keyPath;

数据结构

GNU内部相关数据结构如下所示,下面探究底层实现时会用到


// 静态全局变量

static NSRecursiveLock *kvoLock = nil;

static NSMapTable *classTable = 0;

static NSMapTable *infoTable = 0;

  


// 涉及类型

@interface GSKVOReplacement : NSObject {

  Class         original;       // 原始类

  Class         replacement;    // 替代类

  NSMutableSet  *keys;          // 观察者setter的keys

}

@end

  


@interface GSKVOInfo : NSObject {

  NSObject         *instance; // 观察者(不影响引用计数).

  NSRecursiveLock   *iLock;     // 递归锁

  NSMapTable       *paths;     // 观察的keyPaths map[调用添加方法时传的keyPath: ?]

}

  


// 用于记录一个keyPath的观察结果和发送通知过程的递归状态。

@interface GSKVOPathInfo : NSObject {

@public

  unsigned              recursion;

  unsigned              allOptions;

  NSMutableArray        *observations;

  NSMutableDictionary   *change;

}

@end

  


// 记录一次观察的所有信息。

@interface GSKVOObservation : NSObject {

@public

  NSObject      *observer;      // Not retained (zeroing weak pointer)

  void          *context;

  int           options;

}

@end

底层探究

从添加观察者入手addObserver:forKeyPath:options:context:方法入手【该过程通过kvoLock 加锁】

  1. classTable 中没有存储被观察对象的类对应的替代类(即未创建过),创建一个GSKVOReplacement对象【该过程通过kvoLock 加锁】,存储到classTable中【该过程通过kvoLock 加锁】


/** 

* 替代类GSKVOReplacement的创建

* 创建原始类的子类,并使用抽象基类的实现重写一些方法。

*/

  


superName = NSStringFromClass(original);

// 1. 子类名称为GSKVO拼接原始类名

name = [@"GSKVO" stringByAppendingString: superName];

// 2. 这里用到了runtime的动态性去动态地创建了一个类:objc_registerClassPair

template = GSObjCMakeClass(name, superName, nil);

GSObjCAddClasses([NSArray arrayWithObject: template]);

replacement = NSClassFromString(name);

// 3. 在该方法中,用runtime接口class_copyMethodList 递归 获取原始类的实例方法和类方法,添加到替代类中。

GSObjCAddClassBehavior(replacement, baseClass);

  1. infoTable中没有存储本次观察的信息

    1. 创建一个GSKVOInfo对象【该过程通过kvoLock 加锁】

    2. 存储到infoTable中【该过程通过kvoLock 加锁】,

    3. 将被观察对象设置成第1步中创建的GSKVOReplacement 替代类类型


// a

// 存储被观察对象

instance = i;

// 创建一个容量为8的map,

paths = NSCreateMapTable(NSObjectMapKeyCallBacks,

                         NSObjectMapValueCallBacks,

                         8);

iLock = [NSRecursiveLock new];

  


// b

// 可见infoTable的key为被观察对象的指针地址,value为上面创建的GSKVOInfo对象

NSMapInsert(infoTable, (void*)self, observationInfo);

  


// c

object_setClass(self, [r replacement]);

前3步可以说都是在做准备,下面才开始添加观察者。

  1. 递归获取传入的keyPath中字符”.”的位置调用addObserver:forKeyPath:options:context: 方法,直到keyPath中没有”.”,执行下一步

  2. 替换替代类中对应keyPath属性的setter方法

  3. 调用第3步创建的GSKVOInfoaddObserver:forKeyPath:options:context:方法

    1. 判断观察者是否实现observeValueForKeyPath:ofObject:change:context:,没实现就退出

    2. 从paths中获取传入的keyPath对应的GSKVOPathInfo(没有则创建)

    3. 遍历GSKVOPathInfo中的所有观察者(GSKVOObservation列表),若当前keyPath存在当前观察者,将新的context和options覆盖掉原来的

    4. 若没有GSKVOObservation,则创建,注意:在创建时,使observer实例变量弱引用观察者

    5. 若options中存在NSKeyValueObservingOptionInitial,向观察者发送现有值的通知(key为NSKeyValueObservingOptionNew


// c

while (count-- > 0) {

    GSKVOObservation *o;

    

    o = [pathInfo->observations objectAtIndex: count];

    if (o->observer == anObserver) {

        o->context = aContext;

        o->options = options;

        observation = o;

    }

    pathInfo->allOptions |= o->options;

}

  


// d

observation = [GSKVOObservation new];

// 弱引用,不会影响观察者的引用计数

GSAssignZeroingWeakPointer((void**)&observation->observer,

                           (void*)anObserver);

  


// e

[pathInfo->change setObject: [NSNumber numberWithInt: 1]

                     forKey:  NSKeyValueChangeKindKey];

// 调用观察者实现的observeValueForKeyPath:ofObject:change:context: 观察方法

[anObserver observeValueForKeyPath: aPath

                          ofObject: instance

                            change: pathInfo->change

                           context: aContext];

NSNotificationCenter

基础

  • 中心化的注册和广播逻辑

  • 主要关注系统性事件,而不是特有对象的事件

  • 常用于:前后台切换 内存警告

The Swift overlay to the Foundation framework provides the Notification structure, which bridges to the NSNotification class.(Swift覆盖到Foundation框架提供了Notification结构,它桥接到OC的NSNotification类。)

 

NSNotificationCenter基本用法如下


/** 向NSNotificationCenter注册观察者 */

// 注:该返回值将被系统保留。调用者应该持有该值,以便使用removeObserver删除观察者:稍后,停止观察。

- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name

                             object:(nullable id)obj

                              queue:(nullable NSOperationQuzeue *)queue

                         usingBlock:(void (NS_SWIFT_SENDABLE ^)(NSNotification *note))block;

  


- (void)addObserver:(id)observer

           selector:(SEL)aSelector

               name:(nullable NSNotificationName)aName

             object:(nullable id)anObject;

  


/** 移除观察者 */

- (void)removeObserver:(id)observer;

- (void)removeObserver:(id)observer

                  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;

对象向NSNotificationCenter注册以接收通知(NSNotification)。当对象将自己添加为观察者时,它将指定应该接收哪些通知。因此,一个对象可以多次调用此方法,以便将自己注册为多个不同通知的观察者。

每个正在运行的应用程序都有一个defaultCenter,也可以创建新的NSNotificationCenter来组织特定上下文中的通信。

NSNotificationCenter不保留观察者或对象。因此,在释放这些对象之前,你应该总是发送removeObserver:或removeObserver:name:object:到通知中心。

 

通知中心只能在单个程序中发送通知。如果想发布一个通知到其他进程或从其他进程接收通知,使用NSDistributedNotificationCenter

 

数据结构

NSNotificationCenter数据结构如下,在NSNotificationCenter内部存有一个成员变量_table,本质是NSTable 结构体,其中存储了一个Observation结构体,根据其中的struct Obs *next可以看出观察者是一个****链表结构。** **


@interface NSNotificationCenter : NSObject

{

@private

  void *_table;

}


typedef struct NCTbl {

  Observation *wildcard; // 若name和object都为空,观察者存储在这里

  GSIMapTable nameless; // 若name为空,object不为空,观察者存储在这里

  GSIMapTable named; // 若name不为空,观察者存储在这里

  NSRecursiveLock *_lock; // 用NSRecursiveLock来保证线程安全

  ···

} NCTable;

  


// Observation观察着对象(GNU内部结构)

typedef struct Obs {

  id observer; // 接收消息的对象

  SEL selector; // 接收消息的方法selector

  struct Obs *next; // 指向下一个观察者

···

} Observation;

  


// named对应的GSIMapTable结构

struct _GSIMapTable {

    GSIMapBucket buckets; /* Array of buckets. */

    GSIMapNode freeNodes; /* List of unused nodes. */

    GSIMapNode *nodeChunks; /* Chunks of allocated memory. */

···

};

NSNotification数据结构如下


@interface NSNotification : NSObject <NSCopying, NSCoding>

  


@property (readonly, copy) NSNotificationName name;

@property (nullable, readonly, retain) id object;

@property (nullable, readonly, copy) NSDictionary *userInfo;

  


@end

底层探究

我们以添加观察者的addObserver:selector:name:object:方法为例探究

  1. 加锁NSRecursiveLock ,保证线程安全

  2. 创建观察者对象Observation o,存储传入的selectorobserver 的指针地址

  3. 若参数name 不为空,执行如下处理


// 1. 从center的named中以name(hash)为key,获取对应的GSIMapTable【以下简称nameTable】

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

  


if (n == 0) {

// 1.1 若不存在,则创建,并存储到链表中

    m = mapNew(TABLE);

    // 注意:由于这是对name第一次添加观察,所以获取name的copy,确保不会出现外部修改name值导致的问题。

    name = [name copyWithZone: NSDefaultMallocZone()];

    GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);

···

} else {

// 1.2 若存在,则拿到nameTable中的GSIMapTable的指针地址

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

}

  


// 2. 以传入的object为key,找到对应结点GSIMapNode

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

if (n == 0) {

// 2.1 若不存在,则创建,并存储到nameTable中

    o->next = ENDOBS;

    GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);

} else {

// 2.2 若存在,将observation插入到对应链表头部

    list = (Observation*)n->value.ptr;

    o->next = list->next;

    list->next = o;

}

  1. 若参数name为空,object不为空,从namelessmap中找到对应链表并存储(流程同上)

  2. 若参数nameobject都为空,即代表接收所有消息,使NSNotificationCenterwildcard 指向该Observation对象o

同理可推得,在发布通知的时候,也是根据上述的数据结构和流程取得对应Observation对象,并执行selector对应的方法,如下所示。


[o->observer performSelector: o->selector

                  withObject: notification];

对比

KVO

  • 优点

    - 可以获取到被观察对象的新旧值变化

    - 可以观察嵌套对象

  • 缺点

    - 只能观察属性的变化

    - 观察的属性只能用NSString来定义,无法进行编译检查

NotificationCenter

  • 优点

    - 无需获取到被观察对象就能添加观察者

    - 可以观察自定义事件,而不一定是属性

    - 对于一个发出的通知,多个对象能够做出反应,即1对多的方式实现简单

  • 缺点

    - 需要被观察对象主动抛出事件

    - 需要及时移除观察者不然可能导致崩溃