【OC源码】KVO 实现原理探究

对源码探究部分写在这: 其他: KVO源码分析(GNUStep 版)
本文主要弄清楚苹果是怎么实现 KVO 机制的, 其中部分知识涉及到 runtime, 可以看我这个专题 二. runtime 怎么实现封装 | runtime 的基础数据结构

正式开始前,先上一张总的流程图, 图中不同颜色代表不同函数, 这里主要聊流程. 源码分析部分在另一篇文章中

KVO 是怎么实现的

在我们已知的 KVO 使用中, 它会先注册监听, 然后在”对象”属性发生变化时, 发出相应的通知.
KVO 实现监听行为, 分为两步:

  1. 将对象的 isa 指向改为新生成的一个类
  2. 将类中, 对应字段的 Setter 方法, 改为带通知的

(技术名词称之为isa-swizzling)
结合 runtime 的知识, 我们知道

  • ”对象”本身是只带 isa 字段的结构体指针.struct objc_object *obj
  • “对象”的 isa 指向另一个结构体变量
  • 这个变量中存放了”对象”的执行函数表, 成员变量表等等.

所以, 只要修改 isa 的指向, 我们就能做到让一个对象执行函数时, 不走原先函数的办法.
同时, KVO 的目的不是改变我们声明好的类与对象. 所以其使用的方式是, 创建一个继承于原类的子类. 这样能保证新生成的类行为与之前的一致.
最后, 再把 KVO 想要达到的目的, “在属性修改时发出通知”. 所直接关联的属性 Setter 函数做修改, 偷梁换柱成自己定好的即可
比如将一个字符属性 setter, 改为setterChar, 内部这样实现

 // pre setting code here  
  [self willChangeValueForKey: key];  
  (*imp)(self, _cmd, val);  
  // post setting code here  
  [self didChangeValueForKey: key];  

KVO 在源码中的实现方式

首先, Foundation 没有开源, 后续对 KVO 的探索都基于同源的 GNUStep (不记得什么地方, 官方提到过, 两边的接口是尽量保持一致的)
所以下面的流程并非基于 Foundation 源码, 仅作为参照, 了解 KVO 实现思路用

主体流程就是最上面的图, 具体源码分析这里不谈, 主要还看这里:
其他: KVO源码分析(GNUStep 版)

缓存:

KVO 提供了一系列的全局静态变量, 用于记录当前监听过的 class, 监听过的信息info, 是否初始化过kvoLock
在实际的运行中, 一旦发现有缓存在, 便不会重新执行一次生成逻辑, 而是改为从已有的缓存中获取

全局有一个叫 observationInfo的东西, 里面存放了监听者的信息.
要注意的跟 KVO 有关的很多数据结构都被标记为 not retained, 这是为何 obc 销毁前没取消监听会 crash 的原因

/* An instance of this records all the information for a single observation.  
 */  
@interface	GSKVOObservation : NSObject  
{  
@public  
  NSObject      *observer;      // Not retained (zeroing weak pointer)  
  void          *context;  
  int           options;  
}  
@end  
/*  
 * Instances of this class are created to hold information about the  
 * observers monitoring a particular object which is being observed.  
 */  
@interface	GSKVOInfo : NSObject  
{  
  NSObject	        *instance;	// Not retained.  
  NSRecursiveLock	        *iLock;  
  NSMapTable	        *paths;  
}  

流程:

  1. 初始化全局缓存的初始化, 调用函数 setup()
  2. 生成替换类: 使用 self, 使用字符串拼接的方式生成对应 替换类, 调用replacementForClass
  3. 使用 self, 创建对应的类 调用 runtime 中的 objc_allocateClassPair
  4. copy self 中的成员变量表,(while 遍历实现)
  5. copy self 中的函数表
  6. 后续代码中没有看到 copy 其他内容(这还挺奇怪的)
  7. 将 isa 改为替换类: 调用object_setClass实现
  8. 根据监听的属性类型, 替换属性的 setter 方法

从实现方式中得出的一些点

  • 流程中生成的替换类是原类的子类
  • 因为 KVO 是通过重写 setter 实现的, 并且新的 setter 中会调用原类的 setter, 所以
    • 所有通过 setter 触发的属性修改, 都会触发监听, 包括 KVC 设值
    • 对于没有 setter 的成员变量无效
  • KVO 的通知是通过 setter 中willChangeValueForKey, didChangeValueForKey 实现, 所以如果手动调用这两个函数是可以触发对应的监听方法的
  • 实际操作中, 修改的 set 函数包含 set 两种 _set