一、KVO的本质
1. 创新一个新类
利用runtime的API动态生成一个子类,并且让实例对象的isa指针指向这个全新的子类。
swift
self.person1 = [[GSPerson alloc] init];
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person.age changed"];
NSLog(@"类对象 -> %@", object_getClass(self.person1));
输出:类对象 -> NSKVONotifying_GSPerson
系统创建了一个以NSKVONotifying_
开头的子类,实例对象的isa指针指向这个子类。
2. 实现逻辑
当修改实例对象的属性(调用set方法),会调用Foundation的_NSSetxxxValueAndNotify
函数,函数内部会调用:
- willChangeValueForKey:
-
super setXXX:
- didChangeValueForKey:(方法内部会触发监听器的监听方法observeValueForKeyPath:ofObject:change:context:)
less
NSLog(@"添加observer之前---setAge:方法的调用位置:%p", [self.person1 methodForSelector:@selector(setAge:)]);
[self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person.age changed"];
NSLog(@"添加observer之后---setAge:方法的调用位置:%p", [self.person1 methodForSelector:@selector(setAge:)]);
输出:
添加observer之前---setAge:方法的调用位置:0x1022317e8
添加observer之后---setAge:方法的调用位置:0x1078e7f48
当person1添加了KVO之后,setAge:方法的调用位置发生变化,后面的调用位置是Foundation类的_NSSetIntValueAndNotify
方法。
疑问:_NSSetIntValueAndNotify方法里面做了什么?
验证方法:增加监听方法和触发KVO的点击事件
objectivec
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {
NSLog(@"%@.%@ is changed, info: %@, value:\n%@", object, keyPath, context, change);
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person1.age = 20;
}
当触发了KVO,输出内容如下:
ini
-[GSPerson willChangeValueForKey:]
-[GSPerson setAge:]
didChangeValueForKey: -- begin
<GSPerson: 0x600003bc9690>.age is changed, info: Person.age changed, value:
{
kind = 1;
new = 20;
old = 0;
}
didChangeValueForKey: -- end
发现触发了3个方法:willChangeValueForKey:、setAge、didChangeValueForKey:,说明_NSSetIntValueAndNotify函数里面调用了这几个方法,并且,在didChangeValueForKey:方法里面调用了监听方法observeValueForKeyPath:ofObject:change:context:
二、手动触发KVO
- 只有调用了setAge:才会触发KVO,如何在不调用setAge:方法的前提下触发KVO呢?
答:调用willChangeValueForKey: 和 didChangeValueForKey:方法,传入对应的key即可。
csharp
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
- 直接修改成员变量(_age)会触发KVO吗?
不会,直接修改成员变量并没有调用setAge:方法。
三、附加知识点
- 获取类对象的所有方法
遍历类对象的方法列表
ini
#import <objc/runtime.h>
- (void)printMethodNamesOfClass:(Class)cls {
NSMutableString* methodNames = [NSMutableString string];
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
for (NSUInteger i = 0; i < count; i ++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendFormat:@"%@, ",methodName];
}
NSLog(@"%@的所有方法:%@", cls, methodNames);
}
[self printMethodNamesOfClass:object_getClass(self.person1)];
输出:NSKVONotifying_GSPerson的所有方法:setAge:, class, dealloc, _isKVOA
- 为什么NSKVONotifying_GSPerson类要重写class方法?
直接返回GSPerson类,以屏蔽KVO的内部实现,隐藏NSKVONotifying_GSPerson类。
正因为这样,当self.person1添加了KVO,如果用[self.person1 class]方法获取类对象,发现输出的是GSPerson类对象,只有使用运行时方法object_getClass(self.person1)才能拿到正确的类NSKVONotifying_GSPerson。