KVO的原理是什么?底层是如何实现的?
KVO是Key-value observing的缩写。
KVO是Objective-C是使用观察者设计模式实现的。
Apple使用了isa混写(isa-swizzling)来实现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 = ;
self.person2.age = ; // 添加监听之前,获取类对象,通过两种方式分别获取 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]);
}
输出:
-- ::13.276167+ KVO原理[:] before getClass--->> p1:XGPerson p2:XGPerson
-- ::13.276271+ KVO原理[:] before class--->> p1:XGPerson p2:XGPerson -- ::13.276712+ KVO原理[:] after getClass--->> p1:NSKVONotifying_XGPerson p2:XGPerson
-- ::13.276815+ KVO原理[:] 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:)]);
输出:
-- ::13.276402+ KVO原理[:] person1添加KVO监听之前 - 0x10277c3e0 0x10277c3e0 -- ::17.031319+ KVO原理[:] 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,输出如下:
-- ::24.788395+ KVO原理[:] willChangeValueForKey
-- ::24.788573+ KVO原理[:] XGPerson setAge
-- ::24.788696+ KVO原理[:] didChangeValueForKey - begin
-- ::24.788893+ KVO原理[:] 监听到<XGPerson: 0x60400022f420>的age属性值改变了 -
-- ::24.789014+ KVO原理[:] 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=; 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]];
输出:
-- ::07.918209+ KVO原理[:] NSKVONotifying_XGPerson对象-所有方法:| setAge:| class| dealloc| _isKVOA
-- ::07.918371+ KVO原理[:] 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吗?
答案是不会的,为什么呢?因为KVO是通过修改set方法实现来触发的,一个成员变量都没有 set 方法,所以肯定是不会触发了.
总结
KVO是通过runtime机制动态的给要添加KVO监听的对象创建一个子类,并且让instance对象的isa指向这个全新的子类.
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数,顺序如下:
- willChangeValueForKey:
- 父类原来的setter
- didChangeValueForKey:
didChangeValueForKey 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
通过这个子类重写一些父类的方法达到触发KVO回调的目的.
补充
KVO是使用了典型的发布订阅者设计模式实现事件回调的功能,多个订阅者,一个发布者,简单的实现如下:
1> 订阅者向发布者进行订阅.
2> 发布者将订阅者信息保存到一个集合中.
3> 当触发事件后,发布者就遍历这个集合分别调用之前的订阅者,从而达到1对多的通知.
以上已全部完毕,如有什么不正确的地方大家可以指出~~ ^_^ 下次再见~~