一、 KVO (Key-Value Observing)
KVO 是 Objective-C 对观察者模式(Observer Pattern)的实现。也是 Cocoa Binding 的基础。当被观察的某个属性发生更改时,观察者会获得通知。
- kvo的代码实践
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// //KVO 原理
Person *p1 = [[Person alloc]init];
p1.age = 10;
Person *p2 = [[Person alloc]init];
//监听p2对象的age属性
[p2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
//查看添加监听后对象的变化
[self getMethodName:p1];
[self getMethodName:p2];
//添加监听后 类 和 父类 的变化
NSLog(@"p1-Class:%@",[p1 class]);
NSLog(@"p2-Class:%@",[p2 class]);
NSLog(@"p1-SupperClass:%@",class_getSuperclass(object_getClass(p1)));
NSLog(@"p2-SupperClass:%@",class_getSuperclass(object_getClass(p2)));
NSLog(@"=============");
//修改p2对象的属性
p2.age = 50;
}
- (void)getMethodName:(id)obj
{
NSLog(@"===============");
unsigned int count = 0;
Method *ms = class_copyMethodList(object_getClass(obj), &count);
NSLog(@"Objc:%@",obj);
NSLog(@"isa:%@", object_getClass(obj));
for(int i = 0;i<count;++i){
// struct objc_method_description *des = method_getDescription(ms);
SEL s = method_getName(*ms);
IMP imp = method_getImplementation(*ms);
NSLog(@"%s===%p",sel_getName(s),imp);
++ms;
}
NSLog(@"===============");
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);
}
输出:
二、KVO实现原理分析
KVO是基于runtime机制实现的 当某个被观察对象的属性第一次被观察时,系统就会在运行时期间动态的创建一个派生类,该类派生自被观察对象的类,在这个派生类中重写基类中被观察属性的setter方法。派生类在被重写的setter方法内调用NSObject 的两个方法:,willChangeValueForKey:和 didChangevlueForKey:来实现真正的通知机制。
添加监听前后对象发生的变化
前后对象isa指针的变化
查看上面代码再控制台的输出我们可以看出对象p1和p2两个对象 虽然都是Person类实例化对象,但是p2对象在添加个kvo之后,p2的isa指针却指向一个新的类对象NSKVONotifying_Person。由此可见系统在运行时会创建一个新的继承Person类的子类NSKVONotifying_Person,并将p2对象的isa指针指向了这个新的类对象。
在调用的setAge方法的时候的,通p2的isa指针找到新的NSKVONotifying_Person,并调用里面的重写的setAge方法,触发监听的方法
经过查阅资料我们可以了解到。 NSKVONotifyin_Person中的setage方法中其实调用了 Fundation框架中C语言函数_NSsetIntValueAndNotify,_NSsetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey 将要改变方法,之后调用父类的setage方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。
验证KVO真的如上面所讲的方式实现
首先经过之前打断点打印isa指针,我们已经验证了,在执行添加监听的方法时,会将isa指针指向一个通过runtime创建的Person的子类NSKVONotifyin_Person。 另外我们可以通过打印方法实现的地址来看一下p1和p2的setage的方法实现的地址在添加KVO前后有什么变化。
NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"添加KVO监听之后 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
输出:
我们发现在添加KVO监听之前,p1和p2的setAge方法实现的地址相同,而经过KVO监听之后,p1的setAge方法实现的地址发生了变化,我们通过打印方法实现来看一下前后的变化发现,确实如我们上面所讲的一样,p1的setAge方法的实现由Person类方法中的setAge方法转换为了C语言的Foundation框架的_NSsetIntValueAndNotify函数。
Foundation框架中会根据属性的类型,调用不同的方法。例如我们之前定义的int类型的age属性,那么我们看到Foundation框架中调用的_NSsetIntValueAndNotify函数。那么我们把age的属性类型变为double重新打印一遍
我们发现调用的函数变为了_NSSetDoubleValueAndNotify,那么这说明Foundation框架中有许多此类型的函数,通过属性的不同类型调用不同的函数。 那么我们可以推测Foundation框架中还有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函数。 我们可以找到Foundation框架文件,通过命令行查询关键字找到相关函数
NSKVONotifyin_Person内部结构
通过上面的对象内部结构输出可以看出NSKVONotifyin_Person除了实现了set、get方法之外还实现了其他方法
- Class
- dealloc
- _isKVOA
如何手动触发KVO
Person *p1 = [[Person alloc] init];
p1.age = 1.0;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
[p1 removeObserver:self forKeyPath:@"age"];
输出:
通过打印我们可以发现,didChangeValueForKey方法内部成功调用了observeValueForKeyPath:ofObject:change:context:,并且age的值并没有发生改变。