KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。
1.KVO的使用
代码如下:
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
----使用----
@interface ViewController ()
@property (nonatomic, strong) Person *p1;
@property (nonatomic, strong) Person *p2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.p1 = [[Person alloc] init];
self.p1.age = 1;
self.p2 = [[Person alloc] init];
self.p2.age = 2;
//给对象p1的age添加监听
NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.p1 addObserver:self forKeyPath:@"age" options:option context:nil];
}
//修改属性的值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.p1.age = 10;
self.p2.age = 20;
}
//监听的回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"object = %@, keyPath = %@ change = %@", object, keyPath, change);
}
打印的结果如下:
可知,监听到了p1对象中age值的改变。
2.疑问
从上面的那段代码中,p1 和 p2都设置了age的值,相当于调用了setAge:这个方法,而setAge: 这个方法是实现是一样。那么为什么p1的setAge:这个方法执行完后会调用了监听的回调,而p2调用了setAge:的方法不会有监听的回调?
要解决这个疑问,我们需要去了解一下p1 和 p2的 类型,打个断点,查看p1 和 p2的isa指针类型如下:
打印的结果可以看出,p1的isa指针指向的不是Person的类对象,而是NSKVONotify_Person的类对象。也就是说,OC在运行时动态地创建了一个NSKVONotify_Person类,而这个类是继承于Person。
一般Person的对象,其内存布局如下:
但是添加了监听后的Person对象,其内存布局就变成了如下:
当p1调用setAge的方法时,不是直接调用Person类对象中的setAge:这个方法,而是先执行NSKVONotifying_Person类对象中的setAge:这个方法,这个方法调用的是Foundation中的_NSSetIntValueAndNotify这个方法。_NSSetIntValueAndNotify这个实现的逻辑大概如下:
(1)调用willChangeValueForKey: 通知开始用修改key的值了
(2)用super调用父类的setAge:的方法,即调用了Person的setAge:的方法
(3)调用didChangeValueForKey: 通知已经修改完key的值了。
3.验证
可以通过methodForSelector:这个方法来获取setAge:的地址,通过地址查看setAge:调用了哪个方法:
NSLog(@"%p %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)] );
在命令行中用p (IMP)xxx地址,查看:
4.查看子类的方法
NSKVONotifying_Person除了重新了setAge:方法外,还有哪些方法呢?我们可以通过runtime的代码来获取:
- (void)getClassMethod:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableArray *methodArray = [NSMutableArray array];
for (int i = 0; i < count; i++) {
Method met = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(met));
[methodArray addObject:methodName];
}
NSLog(@"%@--%@", cls, methodArray);
free(methodList);
}
-------调用如下:
[self getClassMethod:object_getClass(self.p1)];
[self getClassMethod:object_getClass(self.p2)];
查看结果如下:
runtime除了获取方法外,还提供了其他的接口获取其他的信息,如下图:
5.总结
注意:
如果直接修改成员变量是不会调用KVO的,因为kvo的本质是setter方法触发的。但是手动调用willChangeValueForKey和didChangeValueForKey这两个方法也会触发。