iOS常见的音讯告诉机制有如下三种,delegate不多赘述,本篇主要探求KVONSNotification的底层原理完成及比照。

源码地址:gnu/lib-base

方式 delegate KVO NSNotificationCenter
告诉方式 1对1 一对多 一对多
适用言语 OC+Swift OC+承继自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 theNotificationstructure, which bridges to theNSNotificationclass.(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对多的方式完成简单

  • 缺陷

– 需求被调查目标主动抛出事情

– 需求及时移除调查者否则或许导致溃散