KVO 的原理是什么? 底层是如何实现的?
我们可以通过代码去探索一下.
创建自定义类: XGPerson
- @interface XGPerson : NSObject
- @property (nonatomic,assign) int age;
- @property (nonatomic,copy) NSString* name;
- @end
我们的思路就是看看对象添加 KVO 之前和之后有什么变化, 是否有区别, 代码如下:
- @interface ViewController ()
- @property (strong, nonatomic) XGPerson *person1;
- @property (strong, nonatomic) XGPerson *person2;
- @end
- - (void)viewDidLoad {
- [super viewDidLoad];
- self.person1 = [[XGPerson alloc]init];
- self.person2 = [[XGPerson alloc]init];
- self.person1.age = 1;
- self.person2.age = 10;
- // 添加监听之前, 获取类对象, 通过两种方式分别获取 p1 和 p2 的类对象
- NSLog(@"before getClass--->> p1:%@ p2:%@",object_getClass(self.person1),object_getClass(self.person2));
- NSLog(@"before class--->> p1:%@ p2:%@",[self.person1 class],[self.person2 class]);
- // 添加 KVO 监听
- NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
- [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];
- // 添加监听之后, 获取类对象
- NSLog(@"after getClass--->> p1:%@ p2:%@",object_getClass(self.person1),object_getClass(self.person2));
- NSLog(@"after class--->> p1:%@ p2:%@",[self.person1 class],[self.person2 class]);
- }
输出:
2018-11-02 15:16:13.276167+0800 KVO 原理[4083:170379] before getClass--->> p1:XGPerson p2:XGPerson
2018-11-02 15:16:13.276271+0800 KVO 原理[4083:170379] before class--->> p1:XGPerson p2:XGPerson
2018-11-02 15:16:13.276712+0800 KVO 原理[4083:170379] after getClass--->> p1:NSKVONotifying_XGPerson p2:XGPerson
2018-11-02 15:16:13.276815+0800 KVO 原理[4083:170379] after class--->> p1:XGPerson p2:XGPerson
从上面可以看出, object_getClass 和 class 方式分别获取到的 类对象竟然不一样, 在对象添加了 KVO 之后, 使用 object_getClass 的方式获取到的对象和我们自定义的对象不一样, 而是 NSKVONotifying_XGPerson, 可以怀疑 class 方法可能被篡改了.
最终发现 NSKVONotifying_XGPerson 是使用 Runtime 动态创建的一个类, 是 XGPerson 的子类.
看完对象, 接下来我们来看下属性, 就是被我们添加了 KVO 的属性 age, 我们要触发 KVO 回调就是去给 age 设置个值, 那它肯定就是调用 setAge 这个方法.
下面监听下这个方法在被添加了 KVO 之后有什么不一样.
- NSLog(@"person1 添加 KVO 监听之前 - %p %p",
- [self.person1 methodForSelector:@selector(setAge:)],
- [self.person2 methodForSelector:@selector(setAge:)]);
- // 添加 KVO 监听
- NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
- [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];
- NSLog(@"person1 添加 KVO 监听之后 - %p %p",
- [self.person1 methodForSelector:@selector(setAge:)],
- [self.person2 methodForSelector:@selector(setAge:)]);
输出:
2018-11-02 15:16:13.276402+0800 KVO 原理[4083:170379] person1 添加 KVO 监听之前 - 0x10277c3e0 0x10277c3e0
2018-11-02 15:16:17.031319+0800 KVO 原理[4083:170379] person1 添加 KVO 监听之后 - 0x102b21f8e 0x10277c3e0
看输出我们能发现, 在监听之前两个对象的方法所指向的物理地址都是一样的, 添加监听后, person1 对象的 setAge 方法就变了, 这就说明一个问题, 这个方法的实现变了, 我们再通过 Xcode 断点调试打印看下到底调用什么方法
断点后, 在调试器中使用 po 打印对象
- (lldb) po [self.person1 methodForSelector:@selector(setAge:)]
- (Foundation`_NSSetIntValueAndNotify)
- (lldb) po [self.person2 methodForSelector:@selector(setAge:)]
- (KVO 原理 `-[XGPerson setAge:] at XGPerson.m:13)
通过输出结果可以发现 person1 的 setAge 已经被重写了, 改成了调用 Foundation 框架中 C 语言写的 _NSSetIntValueAndNotify 方法,
还有一点, 监听的属性值类型不同, 调用的方法也不同, 如果是 NSString 的, 就会调用 _NSSetObjectValueAndNotify 方法, 会有几种类型
大家都知道苹果的代码是不开源的, 所以我们也不知道 _NSSetIntValueAndNotify 这个方法里面到底调用了些什么, 那我们可以试着通过其它的方式去猜一下里面是怎么调用的.
KVO 底层的调用顺序
我们先对我们自定义的类下手, 重写下类里面的几个方法:
类实现:
- #import "XGPerson.h"
- @implementation XGPerson
- - (void)setAge:(int)age{
- _age = age;
- NSLog(@"XGPerson setAge");
- }
- - (void)willChangeValueForKey:(NSString *)key{
- [super willChangeValueForKey:key];
- NSLog(@"willChangeValueForKey");
- }
- - (void)didChangeValueForKey:(NSString *)key{
- NSLog(@"didChangeValueForKey - begin");
- [super didChangeValueForKey:key];
- NSLog(@"didChangeValueForKey - end");
- }
重写上面 3 个方法来监听我们的值到底是怎么被改的, KVO 的通知回调又是什么时候调用的
我们先设置 KVO 的监听回调
- // KVO 监听回调
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
- NSLog(@"监听到 %@的 %@属性值改变了 - %@", object, keyPath, change[@"new"]);
- }
我们直接修改 person1 的 age 值, 触发一下 KVO, 输出如下:
2018-11-02 15:38:24.788395+0800 KVO 原理[4298:186471] willChangeValueForKey
2018-11-02 15:38:24.788573+0800 KVO 原理[4298:186471] XGPerson setAge
2018-11-02 15:38:24.788696+0800 KVO 原理[4298:186471] didChangeValueForKey - begin
2018-11-02 15:38:24.788893+0800 KVO 原理[4298:186471] 监听到 < XGPerson: 0x60400022f420 > 的 age 属性值改变了 - 2
2018-11-02 15:38:24.789014+0800 KVO 原理[4298:186471] didChangeValueForKey - end
从结果中可以看出 KVO 是在哪个时候触发回调的, 就是在 didChangeValueForKey 这个方法里面触发的
NSKVONotifying_XGPerson 子类的研究
接下来我们再来研究下之前上面说的那个 NSKVONotifying_XGPerson 子类, 可能大家会很好奇这里面到底有些什么东西, 下面我们就使用 runtime 将这个子类的所有方法都打印出来
我们先写一个方法用来打印一个类对象的所有方法, 代码如下:
- // 获取一个对象的所有方法
- - (void)getMehtodsOfClass:(Class)cls{
- unsigned int count;
- Method* methods = class_copyMethodList(cls, &count);
- NSMutableString* methodList = [[NSMutableString alloc]init];
- for (int i=0; i <count; i++) {
- Method method = methods[i];
- NSString* methodName = NSStringFromSelector(method_getName(method));
- [methodList appendString:[NSString stringWithFormat:@"| %@",methodName]];
- }
- NSLog(@"%@对象 - 所有方法:%@",cls,methodList);
- // C 语言的函数是需要手动释放内存的喔
- free(methods);
- }
下面使用这个方法打印下 person1 的所有方法, 顺便我们再对比下 object_getClass 和 class
- // 一定要使用 object_getClass 去获取类对象, 不然获取到的不是真正的那个子类, 而是 XGPperson 这个类
- [self getMehtodsOfClass:object_getClass(self.person1)];
- // 使用 class 属性获取的类对象
- [self getMehtodsOfClass:[self.person1 class]];
输出:
2018-11-02 15:45:07.918209+0800 KVO 原理[4369:190437] NSKVONotifying_XGPerson 对象 - 所有方法:| setAge:| class| dealloc| _isKVOA
2018-11-02 15:45:07.918371+0800 KVO 原理[4369:190437] XGPerson 对象 - 所有方法:| .cxx_destruct| name| willChangeValueForKey:| didChangeValueForKey:| setName:| setAge:| age
通过结果可以看出, 这个子类里面就是重写了 3 个父类方法, 还有一个私有的方法, 我们 XGPerson 这个类还有一个 name 属性, 这里为什么没有 setName 呢? 因为我们没有给 name 属性添加 KVO, 所以就不会重写它, 这里面确实有那个 class 方法, 确实被重写了, 所以当我们使用 [self.person1 class] 的方式的时候它内部怎么返回的就清楚了.
NSKVONotifying_XGPerson 伪代码实现
通过上面的研究, 我们大概也能清楚 NSKVONotifying_XGPerson 这个子类里面是如何实现的了, 大概的代码如下:
头文件:
- @interface NSKVONotifying_XGPerson : XGPerson
- @end
实现:
- #import "NSKVONotifying_XGPerson.h"
- // KVO 的原理伪代码实现
- @implementation NSKVONotifying_XGPerson
- - (void)setAge:(int)age{
- _NSSetIntValueAndNotify();
- }
- - (void)_NSSetIntValueAndNotify{
- // KVO 的调用顺序
- [self willChangeValueForKey:@"age"];
- [super setAge:age];
- // KVO 会在 didChangeValueForKey 里面调用 age 属性变更的通知回调
- [self didChangeValueForKey:@"age"];
- }
- - (void)didChangeValueForKey:(NSString *)key{
- // 通知监听器, 某某属性值发生了改变
- [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
- }
- // 会重写 class 返回父类的 class
- // 原因: 1. 为了隐藏这个动态的子类 2. 为了让开发者不那么迷惑
- - (Class)class{
- return [XGPerson class];
- }
- - (void)dealloc{
- // 回收工作
- }
- - (BOOL)_isKVOA{
- return YES;
- }
如何手动调用 KVO
其实通过上面的代码大家已经知道了 KVO 是怎么触发的了, 那怎么手动调用呢? 很简单, 只要调用两个方法就行了, 如下:
- [self.person1 willChangeValueForKey:@"age"];
- [self.person1 didChangeValueForKey:@"age"];
但是上面说调用顺序的时候, 好像明明 KVO 是在 didChangeVlaueForKey 里面调用的, 为什么还要调用 willChangeVlaueForKey 呢?
那是因为 KVO 调用的时候会去判断这个对象有没有调用 willChangeVlaueForKey 只有调用了这个之后, 再调用 didChangeVlaueForKey 才能真正触发 KVO
总结
KVO 是通过 runtime 机制动态的给要添加 KVO 监听的对象创建一个子类, 并且让 instance 对象的 isa 指向这个全新的子类.
当修改 instance 对象的属性时, 会调用 Foundation 的_NSSetXXXValueAndNotify 函数, 顺序如下:
willChangeValueForKey:
父类原来的 setter
didChangeValueForKey:
didChangeValueForKey 内部会触发监听器 (Oberser) 的监听方法( observeValueForKeyPath:ofObject:change:context:)
通过这个子类重写一些父类的方法达到触发 KVO 回调的目的.
补充
KVO 是使用了典型的发布订阅者设计模式实现事件回调的功能, 多个订阅者, 一个发布者, 简单的实现如下:
1> 订阅者向发布者进行订阅.
2> 发布者将订阅者信息保存到一个集合中.
3> 当触发事件后, 发布者就遍历这个集合分别调用之前的订阅者, 从而达到 1 对多的通知.
以上已全部完毕, 如有什么不正确的地方大家可以指出~~ ^_^ 下次再见~~
来源: https://www.cnblogs.com/xgao/p/9896769.html