KVO,全称为Key-Value Observing,可以用于监听某个类的属性值的改变
KVO的使用
我们创建一个iOS项目,然后新建一个ZJPerson类
@interface ZJPerson : NSObject
@property (nonatomic, assign) int age;
@end
然后在ViewController这个类中申明ZJPerson属性,在viewDidLoad中给ZJPerson添加KVO
@interface ViewController ()
@property (nonatomic, strong) ZJPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person = [[ZJPerson alloc]init];
_person.age = 10;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[_person addObserver:self forKeyPath:@"age" options:options context:nil];
}
- (void)dealloc {
[_person removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
_person.age = 20;
}
@end
运行代码后,点击模拟器屏幕,输出如下
截屏2020-12-21 21.44.46.png
KVO的底层实现原理
我们先修改下原来的代码,再初始化一个ZJPerson对象
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_person1 = [[ZJPerson alloc]init];
_person1.age = 11;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[_person1 addObserver:self forKeyPath:@"age" options:options context:nil];
_person2 = [[ZJPerson alloc]init];
_person2.age = 12;
}
- (void)dealloc {
[_person1 removeObserver:self forKeyPath:@"age"];
[_person2 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
_person1.age = 21;
_person2.age = 22;
}
@end
运行起来之后,点击屏幕输出如下
截屏2020-12-22 21.19.56.png
可以看到只有person1这个对象触发了KVO的回调,为啥会这样呢?
我们打印两个person对象的isa看下区别
可以看到添加了监听的person1对象的isa指向的是NSKVONotifying_ZJPerson这个类对象,而没有添加的监听的person2对象的isa指向的是ZJPerson类对象
那么NSKVONotifying_ZJPerson这个又是什么东西呢?
NSKVONotifying_ZJPerson是runtime动态创建出来的类,是ZJPerson的子类
person2.pngperson1.png
NSKVONotifying_ZJPerson内部的结构伪代码如下
@implementation NSKVONotifying_ZJPerson
- (void)setAge:(int)age {
_NSSetIntValueAndNotify();
}
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
[self observeValueForKeyPath:@"age" ofObject:self change:nil context:nil];
}
@end
我们通过代码来验证下
在viewDidLoad中更新如下代码
- (void)viewDidLoad {
[super viewDidLoad];
_person1 = [[ZJPerson alloc]init];
_person1.age = 11;
_person2 = [[ZJPerson alloc]init];
_person2.age = 12;
NSLog(@"before ---- person1 class is: %@", object_getClass(_person1));
NSLog(@"before ----person2 class is: %@", object_getClass(_person2));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[_person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"after ----person1 class is: %@", object_getClass(_person1));
NSLog(@"after ----person2 class is: %@", object_getClass(_person2));
}
输出如下
截屏2020-12-24 20.17.35.png
可以看到没有添加的_person2指向的是ZJPerson类对象,添加了KVO的_person1对象,isa确实指向了NSKVONotifying_ZJPerson类对象。
我们继续更新代码
- (void)viewDidLoad {
[super viewDidLoad];
_person1 = [[ZJPerson alloc]init];
_person1.age = 11;
_person2 = [[ZJPerson alloc]init];
_person2.age = 12;
// NSLog(@"before ---- person1 class is: %@", object_getClass(_person1));
// NSLog(@"before ----person2 class is: %@", object_getClass(_person2));
NSLog(@"before ---- person1 method is: %p", [_person1 methodForSelector:@selector(setAge:)]);
NSLog(@"before ---- person2 method is: %p", [_person2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[_person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"after ---- person1 method is: %p", [_person1 methodForSelector:@selector(setAge:)]);
NSLog(@"after ---- person2 method is: %p", [_person2 methodForSelector:@selector(setAge:)]);
// NSLog(@"after ----person1 class is: %@", object_getClass(_person1));
// NSLog(@"after ----person2 class is: %@", object_getClass(_person2));
}
运行后输出如下
截屏2020-12-24 20.34.41.png
可以看到,在添加KVO之前,person1和person2的方法是相同的,再添加KVO之后,person1的方法就变了,我们打个断点调试,看看这个方法是什么,点击屏幕触发断点,在lldb中调试
截屏2020-12-24 20.37.58.png
打印出的方法也确实上面所提到的方法
面试题
- KVO的本质是什么?
- 利用rumtime API来动态生成一个全新的子类,并让添加KVO的instance对象的isa指向这个字类
- 当instance属性改变时,调用_NSSetXXXValueAndNotify(XXX代表属性的类,比如属性为Int类型,方法名就是_NSSetIntValueAndNotify), 此方法内部实现如下
- willChangeValueForKey:
- 父类的set方法
- didChangeValueForKey:
-
如何手动触发KVO?
调用如下方法- willChangeValueForKey:
- didChangeValueForKey:
-
直接修改成员变量会触发KVO吗?
不会
网友评论