用 Runtime 自己动手实现 KVO —— 探究 KVO 的底层实现

KVO 的实现原理

在 Objective-C 中,用 KVO 可以很方便的观察某个属性的值的变化,一有变化可以立刻响应,虽然滥用 KVO 容易踩坑,但是在很多情形下,KVO 还是很好用的。接下来我们来看一看 KVO 是怎么实现的。

我们来写一个例子来研究,创建一个 Simple App 项目,首先写一个 Bird 类:

1
2
3
4
5
6
7
8
// Bird.h
#import <Foundation/Foundation.h>

@interface Bird : NSObject

@property (nonatomic, copy) NSString *name;

@end
1
2
3
4
5
6
// Bird.m
#import "Bird.h"

@implementation Bird

@end

在 ViewController.m 中,添加如下代码:

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
#import "ViewController.h"
#import "Bird.h"

@interface ViewController ()
@property (nonatomic, strong) Bird *b;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

Bird *b = [[Bird alloc] init];
[b addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
self.b = b;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"name = %@", self.b.name);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
i++;
self.b.name = [NSString stringWithFormat:@"%@", @(i)];
}

@end

运行程序,每次点击屏幕后,会修改 b 对象的 name 的值,然后触发 KVO 回调,打印出 name 的值。

1
2
3
2018-02-07 04:55:35.536953+0800 KVOImp[35950:2197387] name = 1
2018-02-07 04:55:36.017987+0800 KVOImp[35950:2197387] name = 2
2018-02-07 04:55:36.209775+0800 KVOImp[35950:2197387] name = 3

Bird *b = [[Bird alloc] init]; 一行设一个断点,重新运行程序,程序中断在这一行上。

让程序往下运行一步,然后在下方查看 b 对象的 isa 指针的值:

这时 isa 的值是 Bird 这个类。

让程序再往下走一步,再次查看 b 对象的 isa 指针:

会发现这时 b 对象的 isa 指针变为了 NSKVONotifying_Bird 这个类。

因此,可以大概知道,KVO 的实现原理为:

在执行 addObserver:selector:name:object: 时,创建了一个被观察对象的子类,并重写了被观察属性的 setter 方法。

下面来通过自己实现 KVO 来看一下具体的细节。

自己动手实现 KVO

下面我们来针对上面例子中 Bird 的 name 来自己实现 KVO。下面的实现中先写死仅对 setName: 方法实现 KVO,来提供一个最简单的实现流程。

1.

创建一个 NSObject 的类别,文件为 NSObject+MyKVOImp.h 和 NSObject+MyKVOImp.m。

2.

在 NSObject+MyKVOImp.h 中,添加两个方法声明:

1
2
3
- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;

- (void)my_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

上面两个方法用于替代系统 KVO 提供的那两个不带前缀的方法。

3.

在 NSObject+MyKVOImp.m 中添加 my_addObserver... 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@", oldClassName];

Class NewClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);

class_addMethod(NewClass, @selector(setName:), (IMP)setName, "v@:@");

objc_registerClassPair(NewClass);

object_setClass(self, NewClass);

objc_setAssociatedObject(self, &kAssociatedKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

对上面代码的步骤进行说明:

1. 拿到当前类名,在前面加上 NSKVONotifying_ 前缀创建一个新的类名。
1
2
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@", oldClassName];
2. 创建一个当前类的子类,并以新类名命名。
1
Class NewClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
3. 为这个新类添加一个方法。
1
class_addMethod(NewClass, @selector(setName:), (IMP)setName, "v@:@");

同时提供了一个名为 setName 的 C 语言函数,相关代码会在后面给出。

先来看看 class_addMethod 的参数:

第一个参数指明了要添加方法的类。
第二个参数指定了添加的方法名为 setName 且带有一个参数。
第三个参数指定了方法实现对应的函数指针,即为 C 函数的函数名。
第四个参数指明了这个 C 函数的返回值和参数的类型。

这里的类型依次为:

返回值类型:v —— 代表 void
第一个参数类型:@ —— 代表对象
第二个参数类型:: —— 代表 SEL
第三个参数类型:@ —— 代表对象

4. 把新创建的子类注册进运行时环境,使其可用。
1
objc_registerClassPair(NewClass);
5. 把调用 my_addObserver 方法的对象的 isa 指针设置为新创建的子类。
1
object_setClass(self, NewClass);
6. 把观察者关联到 self 上,使得可以在 setName 函数中取出。
1
objc_setAssociatedObject(self, &kAssociatedKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

现在来看看 setName 函数的实现:

1
2
3
4
5
6
7
8
9
10
void setName(id self, SEL _cmd, NSString *str) {
Ivar ivar = class_getInstanceVariable([self class], "_name");
object_setIvar(self, ivar, str);

NSObject *observer = objc_getAssociatedObject(self, &kAssociatedKey);

if ([observer respondsToSelector:@selector(my_observeValueForKeyPath:ofObject:change:context:)]) {
[observer my_observeValueForKeyPath:@"name" ofObject:self change:@{NSKeyValueChangeNewKey: str} context:nil];
}
}

我们知道每个 Objective-C 方法都含有两个隐式参数 self 和 _cmd,他们对应 C 函数中的第一个和第二个参数。因此 setName 的函数的第一个和第二个参数必须是 id self 和 SEL _cmd。第三个参数才是作为生成的 Objective-C 方法 setName: 的第一个参数。

上面说到 setName 的返回值类型和参数类型依次为 “v@:@”,可以根据上面的说明检查这里的 setName 函数 的实现是不是如此。

在 setName 的实现中:

1. 修改实例变量的值。
1
2
Ivar ivar = class_getInstanceVariable([self class], "_name");
object_setIvar(self, ivar, str);
2. 获取到关联的 Observer 对象。
1
NSObject *observer = objc_getAssociatedObject(self, &kAssociatedKey);
3. 调用 my_observeValueForKeyPath... 方法。
1
2
3
if ([observer respondsToSelector:@selector(my_observeValueForKeyPath:ofObject:change:context:)]) {
[observer my_observeValueForKeyPath:@"name" ofObject:self change:@{NSKeyValueChangeNewKey: str} context:nil];
}

这样一个简单的 KVO 就实现好了,可以在 ViewController 中调用我们自己实现的方法试试效果:

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
#import "ViewController.h"
#import "Bird.h"
#import "NSObject+MyKVOImp.h"

@interface ViewController ()
@property (nonatomic, strong) Bird *bird;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

self.bird = [[Bird alloc] init];
[self.bird my_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)my_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"name = %@", self.bird.name);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
i++;
self.bird.name = [NSString stringWithFormat:@"%@", @(i)];
}

@end

运行程序,可以看到每次点击屏幕,修改 name 的值后,会触发 my_observeValueForKeyPath 的调用,把 name 的值打印出来:

1
2
3
2018-02-07 10:20:56.200072+0800 KVOImp[37915:2306333] name = 1
2018-02-07 10:20:59.099350+0800 KVOImp[37915:2306333] name = 2
2018-02-07 10:49:46.448505+0800 KVOImp[37915:2306333] name = 3

结语

以上我们介绍了 KVO 的实现原理并自己实现了一个简单的 KVO,实际上 KVO 的实现还是很复杂的,要考虑到很多地方,复杂的实现网上有相关代码,或者看 KVO 源码了解一下。