面试的时候被多次问到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,不至于出错。