介绍
KVO是一种机制,当对象(被观察)的某个属性发生更改时,对象可以获得通知,并作出相应处理。那么他是怎么监听的呢?
原理
KVO是用了isa-swizzling来实现的。当对象被kvo观察的时候,此对象的isa指针会改变,指向一个中间的类,而不是它真正的类。然后重写setter方法。
原理的证明
被kvo监听的对象,isa指针指向的中间类是怎样的?
//1、创建一个Person类,属性变量有age
self.p2 = [[Person alloc]init];
self.p2.age = 1;
//2、输出结果:====p2未监听前,isa指针Person
NSLog(@"====p2未监听前,isa指针%@", object_getClass(self.p2));
//3、p2的age属性添加监听
[self.p2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld context:nil];
//4、输出结果:====p2监听后,isa指针NSKVONotifying_Person
NSLog(@"====p2监听后,isa指针%@", object_getClass(self.p2));
//5、移除监听
[self.p2 removeObserver:self forKeyPath:@"age" context:nil];
//6、输出结果:====p2移除监听后,isa指针Person
NSLog(@"====p2移除监听后,isa指针%@", object_getClass(self.p2));
由上段代码可以看出,p2被监听前,isa指针指向Person类。在添加监听后,isa指针指向了NSKVONotifying_Person类。
因此,不能用isa指针来获取他真正的类,而是通过class方法来获取。
中间类和之前的类是什么关系呢?
将上面的第4步的输出。多输出一个isa指针指向的类的父类。
//====p2监听后,isa指针NSKVONotifying_Person====isa指针指向的类的父类指针是Person
NSLog(@"====p2监听后,isa指针%@====isa指针指向的类的父类指针是%@", object_getClass(self.p2),class_getSuperclass((Class)object_getClass(self.p2)));
由上可看出,生成的中间类NSKVONotifying_Person类是原类Person类的子类。
怎么重写的setter方法来通知呢?
我们手动实现一个KVO来证明~
将kvo自动发送通知改成NO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"age"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
此时,再跑一下程序,会发现当我们改变age的时候,不能再收到通知。也就是以下方法,不会被调用。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@的%@改变了%@", object, keyPath, change);
}
手动发送通知
在Person类中重写setter方法
- (void)setAge: (NSInteger)age {
//在改变值前调用
[self willChangeValueForKey:@"age"];
_age = age;
//在改变值之后调用
[self didChangeValueForKey:@"age"];
}
OK,在重写setter方法之后,我们又能收到通知了~如果我们想只有在年龄值改变的时候,才收到通知。那我们可以将setter方法改成如下:
- (void)setAge: (NSInteger)age {
if (_age != age) {
//在改变值前调用
[self willChangeValueForKey:@"age"];
_age = age;
//在改变值之后调用
[self didChangeValueForKey:@"age"];
}
}
这样,在我们给age赋值和上次一样的时候,我们不会再收到通知。也就是说下面这种情况只会收到两次通知。
self.p2.age = 100;
self.p2.age = 110;
self.p2.age = 110;
我们再给person加个属性变量birthYear,当age变的时候,birthYear跟着变化。那么setter方法可以写成下面这样
- (void)setAge: (NSInteger)age {
if (_age != age) {
//在改变值前调用
[self willChangeValueForKey:@"age"];
[self willChangeValueForKey:@"birthYear"];
NSInteger gap = age - _age;
_age = age;
_birthYear = _birthYear + gap;
//在改变值之后调用
[self didChangeValueForKey:@"age"];
[self didChangeValueForKey:@"birthYear"];
}
}
这样,当age变的时候,birthYear也会跟着相应变化。