Kealdish's Studio.

庖丁解牛KVO(二)——KVO实现原理

字数统计: 1.5k阅读时长: 6 min
2016/04/23 Share

概览

在上一篇文章中,我们已经了解了KVO的使用场景以及使用方法。作为Objective-c中异常强大和重要的特性,我们有必要去了解和探索KVO底层的实现机制。

KVO的实现机制到底是怎么样的呢?依据苹果官方文档的介绍:

Automatic key-value observing is implemented using a technique called isa-swizzling.

The is a pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

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.

总结一下,这段话的关键字是isa-swizzling,当某个对象的属性被另一个对象观察时,被观察者的isa指针会被修改,指向一个中间类而不是原来的类。至于这个中间类到底是怎么样的,文档没有描述,可见苹果并不想暴露KVO的实现细节。所以,要想了解KVO底层的实现原理,还需要自己动手去研究。

实现原理剖析

KVO机制在NSObject类中已经实现,故而所有继承NSObject类的子类均可以直接使用KVO。既然是追本溯源,那就要从剖析NSObject类开始。查阅runtime文件时,发现了以下代码:

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
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

从这段代码中我们可以看出,NSObject只有一个Class类型的isa变量,Class则是objc_class类型的指针,objc_class则是包含isa变量的结构体(实际上NSObject的底层实现比这要复杂得多)。根据文档的注释,Class代表的是一个Objective-c类,而objc_object则代表的是一个类的实例。由此也可推导出isa变量实际上指向的是对象本身所代表的类。

根据前面文档所说,KVO实现依赖的技术叫做isa-swizzling。也就是说KVO在isa上作了文章,那要探究KVO的底层实现,就要去追踪isa,看看在KVO的过程中isa指向的类到底发生了什么。由于isa是类的私有变量,无法直接访问。在查阅了runtime中关于NSOjbect.mm实现文件中找到了方法去间接访问isa变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
return _object_getClass(obj);
}
static inline Class _object_getClass(id obj)
{
if (obj) return obj->isa;
else return Nil;
}

从这段代码中可以看出,我们可以调用object_getClass方法访问对象的isa指针,同时,这段代码也说明了isa本质上就是代表一个对象的类型。

以下是为追踪KVO实现过程编写的测试代码:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface Cat : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *age;
@property (nonatomic,copy) NSString *weight;
@end
@implementation Cat
@end
static NSArray *classMethodNameList(Class c){
NSMutableArray *array = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++)
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
free(methodList);
return array;
}
static void customDescription(NSString *name,id obj){
NSString *str = [NSString stringWithFormat:
@"%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
name,
obj,
class_getName([obj class]),
class_getName(object_getClass(obj)),
[classMethodNameList(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
Cat *A = [[Cat alloc] init];
Cat *B = [[Cat alloc] init];
Cat *C = [[Cat alloc] init];
Cat *D = [[Cat alloc] init];
[A addObserver:A forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[B addObserver:A forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[C addObserver:B forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
customDescription(@"A", A);
customDescription(@"B", B);
customDescription(@"C", C);
customDescription(@"D", D);
}
return 0;
}

简单解释下这段代码,classMethodNameList方法返回的是在运行时类的方法列表,不过,方法列表中并不包含父类的方法。customDescription方法打印的是对象名,对象对应的类,在运行时对象指向的类,以及运行时对象指向的类所实现的所有方法。实例化四个Cat对象A、B、C、D,A、B、C分别以不同的方式去观察,D则没有观察者。

以下是运行的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
A: <Cat: 0x100500aa0>
NSObject class: Cat
libobjc class: NSKVONotifying_Cat
implements methods: <setAge:, setName:, class, dealloc, _isKVOA>
B: <Cat: 0x1005035f0>
NSObject class: Cat
libobjc class: NSKVONotifying_Cat
implements methods: <setAge:, setName:, class, dealloc, _isKVOA>
C: <Cat: 0x100503610>
NSObject class: Cat
libobjc class: NSKVONotifying_Cat
implements methods: <setAge:, setName:, class, dealloc, _isKVOA>
D: <Cat: 0x1005037a0>
NSObject class: Cat
libobjc class: Cat
implements methods: <setWeight:, name, setName:, weight, .cxx_destruct, age, setAge:>

A、B被观察的属性都是”name”,而C被观察的属性是”age”。A、B、C在runtime下所指向的类均是NSKVONotifying_Cat,这个类应该就是isa-swizzling中的中间类。而并没有被观察的D在runtime下锁指向的仍然是Cat类。这说明只有在对象的属性被观察的时候才会动态创建中间类,对象的isa指针也就不再指向原来的类,而是指向这个中间类。被观察的属性中没有包括weight,而动态生成的中间类所实现的方法列表中也没有重写setWeight:方法,说明方法是按需动态重写的。此外,仔细观察,我们发现虽然A、B只有name属性被观察了,但是动态生成的中间类仍然实现了setAge:方法,说明A、B、C在运行时实际上指向的是同一个中间类。另外,方法列表中有一个叫做_isKVOA的方法引起我的注意,这应该是一个私有方法,无从得知关于这个方法更多的实现细节,暂时悬在这里,待日后有机会能解开这个疑惑。

CATALOG
  1. 1. 概览
  2. 2. 实现原理剖析