KVO 的实现原理
在 Objective-C 中,用 KVO 可以很方便的观察某个属性的值的变化,一有变化可以立刻响应,虽然滥用 KVO 容易踩坑,但是在很多情形下,KVO 还是很好用的。接下来我们来看一看 KVO 是怎么实现的。
我们来写一个例子来研究,创建一个 Simple App 项目,首先写一个 Bird 类:
1 | // Bird.h |
1 | // Bird.m |
在 ViewController.m 中,添加如下代码:
1 |
|
运行程序,每次点击屏幕后,会修改 b 对象的 name 的值,然后触发 KVO 回调,打印出 name 的值。
1 | 2018-02-07 04:55:35.536953+0800 KVOImp[35950:2197387] name = 1 |
在 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 | - (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context; |
上面两个方法用于替代系统 KVO 提供的那两个不带前缀的方法。
3.
在 NSObject+MyKVOImp.m 中添加 my_addObserver...
方法的实现:
1 | - (void)my_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { |
对上面代码的步骤进行说明:
1. 拿到当前类名,在前面加上 NSKVONotifying_ 前缀创建一个新的类名。
1 | NSString *oldClassName = NSStringFromClass([self class]); |
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 | void setName(id self, SEL _cmd, NSString *str) { |
我们知道每个 Objective-C 方法都含有两个隐式参数 self 和 _cmd,他们对应 C 函数中的第一个和第二个参数。因此 setName 的函数的第一个和第二个参数必须是 id self 和 SEL _cmd。第三个参数才是作为生成的 Objective-C 方法 setName:
的第一个参数。
上面说到 setName 的返回值类型和参数类型依次为 “v@:@”,可以根据上面的说明检查这里的 setName 函数 的实现是不是如此。
在 setName 的实现中:
1. 修改实例变量的值。
1 | Ivar ivar = class_getInstanceVariable([self class], "_name"); |
2. 获取到关联的 Observer 对象。
1 | NSObject *observer = objc_getAssociatedObject(self, &kAssociatedKey); |
3. 调用 my_observeValueForKeyPath...
方法。
1 | if ([observer respondsToSelector:@selector(my_observeValueForKeyPath:ofObject:change:context:)]) { |
这样一个简单的 KVO 就实现好了,可以在 ViewController 中调用我们自己实现的方法试试效果:
1 |
|
运行程序,可以看到每次点击屏幕,修改 name 的值后,会触发 my_observeValueForKeyPath 的调用,把 name 的值打印出来:
1 | 2018-02-07 10:20:56.200072+0800 KVOImp[37915:2306333] name = 1 |
结语
以上我们介绍了 KVO 的实现原理并自己实现了一个简单的 KVO,实际上 KVO 的实现还是很复杂的,要考虑到很多地方,复杂的实现网上有相关代码,或者看 KVO 源码了解一下。