面试的时候被多次问到KVO,被问起KVO的实现原理只是简单的知道会生成一个中间派生类,改类是原类的子类。然后被追问如果自己实现KVO,要怎么实现这个派生类。被观察的对象在addObserverForKey之后改对象的isa就被指向了派生类,那么[obj description]打印出来的为什么还是原来的类名。
带着这些问题,开始看KVO的源码。
KVO
KVO的源码并不是开源的,所以并不知道苹果是如何实现的,幸好还有一套GNU的实现,可以给我们提供一下思路。GNU的下载地址在这里。
对于派生类,下面代码:1
2obj = [[MyObject alloc] init];
[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
当调用到最后一个方法时,runtime已经生成了一个子类,obj的isa指向新的子类,MyObject是新子类的父类。他们之间的关系如下图。

现在来看看GNU中对这一块的实现。
GNU实现
在GNU的实现中,有两个关键的类:GSKVOReplacement、GSKVOBase。
从NSKeyValueObserving.m中的- (void) addObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath options: (NSKeyValueObservingOptions)options context: (void*)aContext
方法开始看。
1  | GSKVOInfo *info;  | 
在这里,找到里面两行关键函数:r = replacementForClass([self class]); 和 object_setClass(self, [r replacement]);。通过这两个函数obj就可以将自己的isa设置为runtime生成的派生类。而这个派生类体现在代码中就是[r replacement]。查看GSKVOReplacement的定义发现replacement方法返回的就是GSKVOReplacement中的一个名为replacement的Class对象。
接下来看一看这个名为r的GSKVOReplacement对象是如何生成的。看一看static GSKVOReplacement * replacementForClass(Class c)函数是如何实现的。
1  | GSKVOReplacement *r;  | 
在这个函数中,首先执行了静态内联函数setup()。在setup()函数中,初始化了classTable等相关的表。并且初始化了静态变量baseClass,这个baseClass是一个GSKVOBase类对象。
在replacementForClass()函数中通过传入的原Class比如MyObject,在classTable中找寻派生类比如KVO_MyObject。如果没找到,通过GSKVOReplacement的initWithClass:方法新建一个并且插入到classTable中。
那么看一看initWithClass又是如何实现的呢。initWithClass实现代码比较长,这里就贴几句关键代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18original = aClass;
 /*
  * Create subclass of the original, and override some methods
  * with implementations from our abstract base class.
  */
 superName = NSStringFromClass(original);
 name = [@"GSKVO" stringByAppendingString: superName];
 template = GSObjCMakeClass(name, superName, nil);
 GSObjCAddClasses([NSArray arrayWithObject: template]);
 replacement = NSClassFromString(name);
 GSObjCAddClassBehavior(replacement, baseClass);
 /* Create the set of setter methods overridden.
  */
 keys = [NSMutableSet new];
 return self;
根据代码可以看到大致的流程为:
1.通过原类拼接派生类的子类名
2.通过GSObjCAddClasses()创建新类,在该函数中,通过superName反射创建superClass,也就是MyObject的class。通过name和objc_allocateClassPair函数创建一个新的Class,也就是派生类。将生成的class对象包装在NSValue中返回给template。
3.通过GSObjCAddClasses将template里的派生类对象注册好。
4.注册好了以后就可以通过name反射得到派生类的class对象relpacement。
5.通过GSObjCAddClassBehavior(Class receiver, Class behavior)为replacement添加相应的方法。这个函数传入的两个参数分别是派生类和baseClass,也就是GSKVOBase。GSObjCAddClassBehavior(Class receiver, Class behavior)这个函数非常关键,简单看一下其中的关键代码。
1  | unsigned int count;  | 
跳过一些不那么关键的代码后其实非常简单,就是把behavior中的方法列表copy一份到receiver中。其中behavior是GSKVOBase,reveiver是派生类,也就是我们obj的isa最后指向的对象。
那么看一看GSKVOBase是如何实现的吧。
GSKVOBase有两个关键方法
1  | - (Class) class  | 
首先重写了自己的class方法,返回的是自己的superClass,这也就是为什么KVO了obj之后[obj description]打印出来的还是MyObject信息。在setValue:forKey:方法中,通过automaticallyNotifiesObserversForKey:判断该key是否被kvo了,如果被kvo了就插入willChangeValueForKey和didChangeValueForKey两个方法。如果没有,执行原有的set方法即可。也就是说replacement中并没有重写所有的任何的set方法,而是通过这种很巧妙的操作记录一下被kvo的key,然后在该key执行set方法时,插入kvo需要的语句,这一切,都是通过runtime来进行的。
看到这里,文章开头的两个问题其实已经有答案了,GNU的这个实现其实就可以作为自己实现KVO的思路,而苹果在官方文档中关于kvo有这么一句话
1  | You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.  | 
这么说的原因,对照GNU这里的代码看,其实就是因为重写了派生类的Class方法,所以不能依赖派生类的Class来判断类的关系。
总结
对于KVO的实现,GNU给出的这套实现中还有很多细节,但是基本的思想就是有一个模版类GSKVOBase,这个类中定义了一些基本行为,例如Class方法,setValue:forKey:方法,所有的派生类(replacement)的方法列表都来自于该模版类,这样就派生类就可以做成一个轻量级的对象,不用重写特定的set方法,在调用被KVO的对象其它方法时,也能保证调用的是原Class方法列表中的IMP,不至于出错。