带着问题读源码----KVO

面试的时候被多次问到KVO,被问起KVO的实现原理只是简单的知道会生成一个中间派生类,改类是原类的子类。然后被追问如果自己实现KVO,要怎么实现这个派生类。被观察的对象在addObserverForKey之后改对象的isa就被指向了派生类,那么[obj description]打印出来的为什么还是原来的类名。
带着这些问题,开始看KVO的源码。

KVO

KVO的源码并不是开源的,所以并不知道苹果是如何实现的,幸好还有一套GNU的实现,可以给我们提供一下思路。GNU的下载地址在这里
对于派生类,下面代码:

1
2
obj = [[MyObject alloc] init];
[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

当调用到最后一个方法时,runtime已经生成了一个子类,obj的isa指向新的子类,MyObject是新子类的父类。他们之间的关系如下图。

派生类关系图

现在来看看GNU中对这一块的实现。

GNU实现

在GNU的实现中,有两个关键的类:GSKVOReplacementGSKVOBase
NSKeyValueObserving.m中的- (void) addObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath options: (NSKeyValueObservingOptions)options context: (void*)aContext
方法开始看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GSKVOInfo             *info;
GSKVOReplacement *r;
NSKeyValueObservationForwarder *forwarder;
NSRange dot;

setup();
[kvoLock lock];

// Use the original class
r = replacementForClass([self class]);

/*
* Get the existing observation information, creating it (and changing
* the receiver to start key-value-observing by switching its class)
* if necessary.
*/
info = (GSKVOInfo*)[self observationInfo];
if (info == nil)
{
info = [[GSKVOInfo alloc] initWithInstance: self];
[self setObservationInfo: info];
object_setClass(self, [r replacement]);
}

在这里,找到里面两行关键函数:
r = replacementForClass([self class]);object_setClass(self, [r replacement]);。通过这两个函数obj就可以将自己的isa设置为runtime生成的派生类。而这个派生类体现在代码中就是[r replacement]。查看GSKVOReplacement的定义发现replacement方法返回的就是GSKVOReplacement中的一个名为replacement的Class对象。
接下来看一看这个名为rGSKVOReplacement对象是如何生成的。看一看static GSKVOReplacement * replacementForClass(Class c)函数是如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
GSKVOReplacement *r;

setup();
[kvoLock lock];
r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);
if (r == nil)
{
r = [[GSKVOReplacement alloc] initWithClass: c];
NSMapInsert(classTable, (void*)c, (void*)r);
}
[kvoLock unlock];
return r;

在这个函数中,首先执行了静态内联函数setup()。在setup()函数中,初始化了classTable等相关的表。并且初始化了静态变量baseClass,这个baseClass是一个GSKVOBase类对象。
replacementForClass()函数中通过传入的原Class比如MyObject,在classTable中找寻派生类比如KVO_MyObject。如果没找到,通过GSKVOReplacementinitWithClass:方法新建一个并且插入到classTable中。
那么看一看initWithClass又是如何实现的呢。initWithClass实现代码比较长,这里就贴几句关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
original = 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 unsigned int	count;
Method *methods;
Class behavior_super_class = class_getSuperclass(behavior);

...
/* 一些不是很关键的代码 */

/* Add instance methods */
methods = class_copyMethodList(behavior, &count);
BDBGPrintf(" instance methods from %s %u\n", class_getName(behavior), count);
if (methods == NULL)
{
BDBGPrintf(" none.\n");
}
else
{
GSObjCAddMethods (receiver, methods, NO);
free(methods);
}

/* Add class methods */
methods = class_copyMethodList(object_getClass(behavior), &count);
BDBGPrintf(" class methods from %s %u\n", class_getName(behavior), count);
if (methods == NULL)
{
BDBGPrintf(" none.\n");
}
else
{
GSObjCAddMethods (object_getClass(receiver), methods, NO);
free(methods);
}

/* Add behavior's superclass, if not already there. */
if (!GSObjCIsKindOf(receiver, behavior_super_class))
{
GSObjCAddClassBehavior (receiver, behavior_super_class);
}
GSFlushMethodCacheForClass (receiver);

跳过一些不那么关键的代码后其实非常简单,就是把behavior中的方法列表copy一份到receiver中。其中behavior是GSKVOBase,reveiver是派生类,也就是我们obj的isa最后指向的对象。
那么看一看GSKVOBase是如何实现的吧。
GSKVOBase有两个关键方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (Class) class
{
return class_getSuperclass(object_getClass(self));
}

- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
Class c = [self class];
void (*imp)(id,SEL,id,id);

imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];

if ([[self class] automaticallyNotifiesObserversForKey: aKey])
{
[self willChangeValueForKey: aKey];
imp(self,_cmd,anObject,aKey);
[self didChangeValueForKey: aKey];
}
else
{
imp(self,_cmd,anObject,aKey);
}
}

首先重写了自己的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,不至于出错。

参考

KVO原理浅析