认识KVO
问题
KVO的本质是什么?(iOS用什么方式实现对一个对象的KVO?)
如何手动触发KVO?
直接修改成员变量会触发KVO么?
- KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
- (void)viewDidLoad {
[super viewDidLoad];
Person *person1 = [[Person alloc] init];
person1.age = 10;
Person *person2 = [[Person alloc] init];
person2.age = 20;
// self 监听 person1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:NULL];
person1.age = 11;
[person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);
}
//控制台输出内容
监听到<Person: 0x600000005550>的age改变了{
kind = 1;
new = 11;
old = 10;
}
通过上面代码可以看到,在添加监听之后,age属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法。
KVO的底层实现
我们知道复制操作就是调用了set方法,我们可以重写Person类的age的set方法,观察是否是KVO在set方法内部做了一些操作来通知监听者。
- (void)setAge:(int)age {
_age = age;
}
发现即使重写了setAge方法,person1对象和person2对象调用同样的set方法,但是我们发现person1除了调用set方法之外还会另外执行监听器的observeValueForKeyPath方法。
我们知道setAge方法是放在Person类对象里面的,而instance对象的isa指针指向的就是Person类对象,我们分别打印看一下person1和person2的isa的值:
addObserver对person1对象的处理
通过上图我们发现,person1对象执行过addObserver操作之后,person1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifying_Person类对象,这个类对象是OC Runtime运行时动态定义的一个新类,这个新类是Person的子类,其superClass指向Person类对象。而person2对象没有任何改变。
首先我们知道,person2在调用setAge方法的时候,首先会通过person2对象中的isa指针找到Person类对象,然后在类对象中找到setAge方法。然后找到方法对应的实现。
未使用KVO监听的对象
那么person1对象在调用setAge方法的时候,肯定会根据person1的isa找到NSKVONotifying_Person,在NSKVONotifying_Person中找setAge的方法及实现。
使用了KVO监听的对象经过查阅资料我们可以了解到
NSKVONotifying_Person中的setAge方法中其实调用了 Fundation框架中C语言函数 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey 将要改变方法,之后调用父类的setage方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。
NSKVONotifyin_Person内部结构是怎样的?
- (void)printMethodNamesOfClass:(Class)class{
unsigned int count;
//获取方法数组
Method *methodList = class_copyMethodList(class, &count);
NSMutableString *methodNames = [NSMutableString string];
//遍历所有方法
for (int i = 0; i < count; i ++) {
//获得方法
Method method= methodList[i];
//获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
//拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@","];
}
//释放
free(methodList);
//打印方法名
NSLog(@"%@ %@", class, methodNames);
}
- (void)viewDidLoad {
[super viewDidLoad];
Person *person1 = [[Person alloc] init];
person1.age = 10;
Person *person2 = [[Person alloc] init];
person2.age = 20;
// self 监听 person1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:NULL];
person1.age = 11;
[self printMethodNamesOfClass:object_getClass(person1)];
[self printMethodNamesOfClass:object_getClass(person2)];
[person1 removeObserver:self forKeyPath:@"age"];
}
//打印内容分别为:
NSKVONotifying_Person setAge:,class,dealloc,_isKVOA,
Person age,setAge:
通过上述代码我们发现NSKVONotifying_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA。
这里NSKVONotifying_Person重写class方法是为了隐藏NSKVONotifying_Person。不被外界所看到。我们在person1添加过KVO监听之后,分别打印person1和person2对象的class可以发现他们都返回Person。
NSLog(@"%@,%@",[person1 class],[person2 class]);
// 打印结果 Person,Person
如果NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到nsobject,而nsobect的class的实现大致为返回自己isa指向的类,返回person1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,但是apple不希望将NSKVONotifying_Person类暴露出来,并且不希望我们知道NSKVONotifying_Person内部实现,所以在内部重写了class类,直接返回Person类,所以外界在调用person1的class对象方法时,是Person类。这样person1给外界的感觉person1还是Person类,并不知道NSKVONotifying_Person子类的存在。
那么我们可以猜测NSKVONotifying_Person内重写的class内部实现大致为
- (Class) class {
// 得到类对象,在找到类对象父类
return class_getSuperclass(object_getClass(self));
}
最后我们来验证一下什么时候会调用observeValueForKeyPath:ofObject:change:context:方法
//当监听对象的属性值发生改变时,就会调用
- (void)setAge:(int)age
{
NSLog(@"setAge:");
_age = age;
}
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"willChangeValueForKey: - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey: - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey: - end");
}
//打印如下
willChangeValueForKey: - begin
willChangeValueForKey: - end
setAge:
didChangeValueForKey: - begin
监听到<Person: 0x60000000cf90>的age改变了{
kind = 1;
new = 11;
old = 10;
}
didChangeValueForKey: - end
通过打印内容可以看到,确实在didChangeValueForKey方法内部已经调用了observer的observeValueForKeyPath:ofObject:change:context:方法。
问题
KVO的本质是什么?(iOS用什么方式实现对一个对象的KVO?)
-
当一个对象使用KVO监听时,iOS系统会利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
-
子类拥有自己的set方法,当修改instance对象的属性时,内部会调用Foundation的_NSSetXXXValueAndNotify函数,函数内部实现为:
willChangeValueForKey: 父类原来的setter didChangeValueForKey: 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
如何手动触发KVO?
被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。
//手动调用KVO
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
直接修改成员变量会触发KVO么?
不会,因为没有调用setter方法。因为KVO的本质是重写set方法,然后在set方法里依次调用willChangeValueForKey,原来的set方法,didChangeValueForKey,didChangeValueForKey内部会调用observer的observeValueForKeyPath: ofObject: change: context:方法
文中如果有不对的地方欢迎指出。