KVO 全称是Key Value Observing,翻译成键值观察。提供了一种当其它对象属性被修改的时候能通知当前对象的机制。
KVO 的基本使用:
(1)注册指定Key路径的监听器:
/** 参数
* addObserver: 监听对象
* forKeyPath: 监听属性Key
* options: 监听可选项
* NSKeyValueObservingOptionNew: 监听改变后的新值
* NSKeyValueObservingOptionOld: 监听改变后的旧值
* context: 传入的上下文
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
(2)删除指定Key路径的监听器:
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
(3)回调监听:
- (void)observeValueForKeyPath:(NSString *)keyPath //监听的属性值
ofObject:(id)object //监听的对象
change:(NSDictionary<NSString *,id> *)change //值的改变(由options参数决定传入新值或者旧值)
context:(void *)context //传入的上下文内容
值得注意的是:不要忘记解除注册,否则会导致资源泄露。
设置属性
将观察者与被观察者注册好之后,就可以对观察者对象的属性进行操作,这些变更操作就会被通知给观察者对象。注意,只有遵循 KVO 方式来设置属性,观察者对象才会获取通知,也就是说遵循使用属性的 setter 方法,或通过 key-path 来设置:
target.age = 30;
[target setAge:30];
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
下面看一个小 Demo:
我们设想一个场景,当自己住酒店的时候,当酒店给我换房间的时候,我们要得到提醒,才能找对自己的房间。我们依次为例:
//ViewController
#import "ViewController.h"
#import "Person.h"
#import "Room.h"
@interface ViewController ()
@property (nonatomic, strong) Person *person;
@property (nonatomic, strong) Room *room;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
self.room = [[Room alloc] init];
//设置房间的号码
self.room.no = 10;
//Person 监听 Room 编号的变化
[self.room addObserver:self.person forKeyPath:@"no" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//房间号变为20
self.room.no = 20;
}
@end
//Person.m 文件
#import "Person.h"
@implementation Person
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"no"]) {
NSLog(@"Person 检测到 Room 的属性: %@ 值改变: %@", keyPath, change);
}
}
//移除观察者对象,防止内存泄漏
- (void)dealloc{
[self.room removeObserver:self forKeyPath:@"no"];
}
@end
运行,我们得到:
KVO 的内部实现
下面我们分析下,KVO 的内部实现:
1> KVO 是基于 runtime 的 isa-swizzing 机制实现的;
2> 当类 A 的对象第一次被观察的时候,系统会在运行期动态创建类A 的派生类。系统命名为NSKVONotifying_A。
3> 在派生类 NSKVONotifying_A 中重写类 A 的setter方法,NSKVONotifying_A类在被重写的setter方法中实现通知机制。
4> 其中键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后,observeValueForKey:ofObject:change:context: 会被调用,继而 didChangeValueForKey: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了(后面介绍)。
5> 类 NSKVONotifying_A会重写 class方法,将自己伪装成类A。类 NSKVONotifying_A 还会重写 deallo 方法来释放资源。
系统将所有指向类 A 对象的isa指针指向类 NSKVONotifying_A 的对象。
为了证明上述过程:我们第一步注释掉ViewController 添加观察者的代码,在运行的时候,查看类 Room 的 isa 指针的值:
当将添加观察者处的代码打开,我们观察到,在运行的时候,Room 的 isa指针指向了NSKVONotifying_Room 类(派生类)
KVO 手动实现
在 Room.m 文件中实现:
/**
首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;
其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
*/
#import "Room.h"
@implementation Room
- (void)setNo:(int)no{
[self willChangeValueForKey:@"no"];
_no = no;
[self didChangeValueForKey:@"no"];
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"no"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
当我们再次运行观察,发现 Room 的 isa 指针指向Room类:
参考:
http://www.cppblog.com/kesalin/archive/2012/11/17/kvo.html
申明
以上观点,属于个人的理解,如果错误之处,欢迎拍砖。