添加观察方法:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
第一个参数是观察者对象,负责处理监听事件;第二个是观察的属性的路径;第三个是观察的选项;第四个是上下文。
监听回调方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
大部分的参数是和添加方法里的对应的,不同的是观察选项和change参数,但两者是对应的。
options有四个选项
NSKeyValueObservingOptionNew:change会接收到观察属性的新值;
NSKeyValueObservingOptionOld:change会接收到观察属性的旧值;
NSKeyValueObservingOptionInitial:回调方法会在观察属性初始化的时候调用,但不会接收到这个初始值,除非和NSKeyValueObservingOptionNew选项一起使用;
NSKeyValueObservingOptionPrior:回调方法会触发两次,一次是观察属性改变前,一次是改变后,所以可以配合willChangeValueForKey:方法一起使用
比如说大家都喜欢给宠物取名字,养的猫也会有名字
@interface Cat : NSObject
@property (nonatomic, strong) NSString* name;
@end
我们想要在猫的名字改变的时候有个通知,我们在一个viewcontroller里面做测试,其他多余的代码暂时不要
#import "KVOViewController.h"
#import "Cat.h"
@interface KVOViewController ()
@property (nonatomic, strong) Cat* whiteCat;
@end
@implementation KVOViewController
- (void)viewDidLoad {
[super viewDidLoad];
_whiteCat = [[Cat alloc] init];
_whiteCat.name = @"hello";
[_whiteCat addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
- (IBAction)observerTap:(id)sender {
_whiteCat.name = @"kitty";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"white cat's name change");
}
}
@end
我们在当前进行观察的,所以第一个参数观察者是self,当然这不是固定的,可以指到其他地方观察,当都要实现监听方式。
我们是要在猫的名字改变的时候得到事件,所以监听的第二个参数keypath是name。
keypath描述的是要观察的属性,而这个属性,要符合KVC协议的,简单点说,就是这个属性要能够被进行以下操作
- (void)setValue:(id)value forKey:(NSString *)key;
就是说这个属性能够被键值编码,如果我们想观察一个数组的count属性,直接使用
[_obArr addObserver:self forKeyPath:@"count" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
程序会crash的,因为数组的count属性不符合键值编码,即简单的不能被setValue:forKey,因为它不是id类型。
keypath,顾名思义,这是一个寻找的路径,所以这里如果是想观察name的属性也是可以用点语法观察的,当然,前提也是要符合KVO协议。
最后一个参数,一般是传nil或者NULL,但作用呢,其实很强大,比如当前有两个Cat的实例,我都要观察他们的name属性变化,我该如何区分是哪个实例呢?就用这个参数,context。我们给上面的例子添加点代码:
@interface KVOViewController ()
@property (nonatomic, strong) Cat* whiteCat;
@property (nonatomic, strong) Cat* blackCat;
@end
@implementation KVOViewController
- (void)viewDidLoad {
[super viewDidLoad];
_whiteCat = [[Cat alloc] init];
_whiteCat.name = @"hello";
[_whiteCat addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"White"];
_blackCat = [[Cat alloc] init];
_blackCat.name = @"kitty";
[_blackCat addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:@"Black"];
}
- (IBAction)observerTap:(id)sender {
_whiteCat.name = @"kitty";
_blackCat.name = @"hello";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
id instanceObj = (__bridge id)(context);
NSString* contextStr = instanceObj;
if ([contextStr isEqualToString:@"White"]) {
NSLog(@"white cat's name change");
} else if ([contextStr isEqualToString:@"Black"]) {
NSLog(@"black cat's name change");
}
}
}
@end
如果是自己观察自己的属性变化呢,该如何做?下面是我的实验代码:
@interface Cat : NSObject
@property (nonatomic, strong) NSString* name;
- (void)beginToObserver;
- (void)changeName;
@end
@implementation Cat
- (void)beginToObserver {
[self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
- (void)changeName {
_name = @"hello";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"here");
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"cat's name change");
}
}
@end
生成了Cat实例后,调用beginToObserver添加观察,在某处触发改变调用changeName改变名字。
运行的结果是,触发了按钮事件,观察的字符串的值也改变了,但是就是不触发监听回调方法。为什么?
我们再修改一处代码,把字符串的再次赋值方式,变成点语法赋值:
- (void)changeName {
self.name = @"hello";
}
这次运行就触发了监听回调方法了。
添加观察前后,isa指针指向发生了改变,这是在KVO通过runtime创建被观察的class的subclass(通常会以NSKVONotifying前缀),在这个subclass里,set方法会被重写,在set方法里实现了通知机制,所以调用点语法才能触发通知。