简述
KVO是key-value-observe的简称,也就是键值观察者,是一种设计模式 -- 观察者模式。核心思想就是:
被观察者的状态发生改变时,会通知给观察者,观察者在对应的方法里可以获取相关信息。
KVO要完整实现,需要四个要素即:观察者、被观察者、被观察的“变量”以及实现回调方法。
当然,KVO的实现不是死板的,可以通过三种途径实现即:自动KVO、手动KVO以及依赖键KVO。下面通过实例来一个个讲解。
一、准备工作
首先 创建新项目,然后创建一个SecondModel类,并添加name和age两个属性
@interface SecondModel : NSObject
@property(nonatomic, strong)NSString *name;
@property(nonatomic, assign)NSInteger age;
@end
第二步 在ViewController类中引入SecondModel类的头文件,并为ViewController类的class-continuation分类中添加一个SecondModel类型的属性和一个字符串属性。
#import "ViewController.h"
#import "SecondModel.h"
@interface ViewController ()
@property (nonatomic, strong)SecondModel *model;
@property (nonatomic, strong)NSString *firstString;
@end
第三步 了解两个方法。对于上面说的三种KVO,都会用到两个方法,一个是添加观察者的方法,另一个是删除观察者的方法,介绍如下:
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
参数介绍
observer -- 观察者
keyPath -- 被观察者的“变量”,通常就是被观察对象的属性
options -- 在回调方法中我需要获取到的“变量”值,如新值(NSKeyValueObservingOptionNew)、旧值(NSKeyValueObservingOptionOld)等。
context -- 上下文,我理解的是观察者所在的上下文,用于区分有很多观察者时,进行分类处理。传nil也可以。
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(nullable void *)context
或者
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
参数介绍
observer
keyPath
context
三个参数应该连起来讲:被观察者 移除在指定上下文(context)中,其被观察“变量”名为keyPath的观察者(observer)。
二、自动KVO
所谓自动是相对于手动KVO来说的,先用代码实现,后面再说自动手动。
下面根据KVO实现的四要素来完成一个KVO实例
观察者:对象model,
被观察者:ViewController的实例对象,
被观察的“变量”:firstString,
先完成这个三个要素,所以在ViewController类的viewDidLoad中实现代码如下:
[self addObserver:self.model forKeyPath:@"firstString" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];
添加了观察者,那一定要添加移除观察者的代码,养成好习惯,不添加会导致内存泄漏
-(void)dealloc{
[self removeObserver:self.model forKeyPath:@"firstString"];
}
那最后一个要素就是“实现回调方法”,那应该在那里实现呢?谁作为观察者,就在谁那里实现。所以这里应该在SecondModel类里面实现。
#import "SecondModel.h"
@implementation SecondModel
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"keyPath:%@,object:%@,change:%@,context:%@",keyPath,object,change,context);
}
@end
好,到现在条件都已具备,只差触发了,只要改变firstString的值就可以触发。我在ViewController类里面实现touchesBegan:方法
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.firstString = @"you are beautiful";
}
运行项目,点击屏幕,打印如下:
2019-01-25 14:29:16.415324+0800 KVOtest[96959:895162] keyPath:firstString,
object:,
change:{
kind = 1;
new = "you are beautiful";
old = "";
},
context:(null)
这样就可以清楚的看到从回调方法中获取到的数据,根据你的需要进行取舍。
这里多说下这个options,前面添加观察者的时候,写的代码是:
NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
那如果只写
NSKeyValueObservingOptionNew
打印出来是这样子的
2019-01-25 14:49:47.024828+0800 KVOtest[97253:905201] keyPath:firstString,
object:,
change:{
kind = 1;
new = "you are beautiful";
},
context:(null)
很明显change里面没有old字段了。因为这次options 参数填的是NSKeyValueObservingOptionNew。
如果只写
NSKeyValueObservingOptionOld
打印结果如下
2019-01-25 14:53:18.271560+0800 KVOtest[97324:907276] keyPath:firstString,
object:,
change:{
kind = 1;
old = "";
},
context:(null)
这么看的话,也就可以得出结论,填什么option就会返回对应值,值得注意的是,两个option之间用“|”隔开。
自动KVO的解释
自动KVO -- 指的是,在类中添加属性时是自动生成对应getter/setter方法的,那么观察这种属性就是属于自动KVO。上面的实例就是自动KVO。
三、手动KVO
手动KVO其实就是关掉自动KVO,然后自己手动绑定KVO。实现流程如下。
首先 和上面一样,添加一个属性、添加观察者、移除观察者
在ViewController的.m文件中添加代码如下:
@property (nonatomic, strong)NSString *secondString;
[self addObserver:self.model forKeyPath:@"secondString" options:(NSKeyValueObservingOptionOld) context:nil];
[self removeObserver:self.model forKeyPath:@"secondString"];
因为之前已经实现了观察者的回调方法,所以,这里就省略了。
然后 关闭自动KVO,实现代码如下
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
if ([key isEqualToString:@"secondString"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
这个方法是系统提供的,在调用addObserver:forkeyPath:options:context:方法时,就会走到这个方法里面,当对应的keyPath 进到这个方法时,返回NO,那就是关闭自动KVO;同时注意,除此之外的所有keyPath 都让它走父类的方法,这样就不会影响其他的KVO。
到这一步的话,不出意外,当我改变secondString的值时,是不会走
observeValueForKeyPath:...的回调方法的,验证下。
改变secondString的值,如下
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.firstString = @"you are beautiful";
self.secondString = @"white color";
}
打印如下:
2019-01-25 15:56:31.392765+0800 KVOtest[98301:939948] keyPath:firstString,
object:,
change:{
kind = 1;
old = "";
},
context:(null)
(lldb)
并没有secondString相关的信息,对应的自动KVO确实被关闭了。
第三 实现secondString的setter方法,同时一前一后调用changeValueForKey的两个方法,代码如下:
-(void)setSecondString:(NSString *)secondString{
[self willChangeValueForKey:@"secondString"];
_secondString = secondString;
[self didChangeValueForKey:@"secondString"];
}
注意,两个方法都要调用,缺一不可,相当于自己手动绑定KVO。
现在再运行一次,触发touchBegin方法,打印如下:
2019-01-25 16:01:10.751904+0800 KVOtest[98377:942808] keyPath:firstString,
object:,
change:{
kind = 1;
old = "";
},
context:(null)
2019-01-25 16:01:12.251331+0800 KVOtest[98377:942808] keyPath:secondString,
object:,
change:{
kind = 1;
old = "";
},
context:(null)
这样手动KVO就实现了。虽然我也不知道手动KVO有什么好。
依赖键KVO
想象有这么一个场景,一个视图label的显示值是由model的某个属性值决定,即:
label.text = model.a
那么当model 的 a 改变时,通常我们会再执行一遍上面的那一行代码,来给label重新赋值,假如当model的a值改变的时机比较多是,那代码就会有多处出现上面的那行代码,虽然问题不大,但是使用依赖键KVO可以缓解这个问题。
依赖建KVO在这里可以这么解释:
添加一个观察者,观察label,当model.a的值发生改变时,通过依赖键(即label依赖键“model.a”),label.text的值也会发生改变,同时观察者的回调方法会执行。
下面用一个实例来解释:
首先创建一个MessageLabel类,继承自UILabel。在ViewController中添加一个MessageLabel属性
@property (nonatomic, strong)MessageLabel *label;
并初始化
self.label = [[MessageLabel alloc] initWithFrame:CGRectMake(40, 100, [UIScreen mainScreen].bounds.size.width - 80, 50)];
self.label.backgroundColor = [UIColor cyanColor];
[self.view addSubview:self.label];
然后 与之前的步骤一样添加观察者、删除观察者
[self addObserver:self.label forKeyPath:@"label" options:(NSKeyValueObservingOptionNew) context:nil];
-(void)dealloc{
[self removeObserver:self.model forKeyPath:@"firstString"];
[self removeObserver:self.model forKeyPath:@"secondString"];
[self removeObserver:self.label forKeyPath:@"label"];
}
同时在MessageLabel类中添加回调方法
#import "MessageLabel.h"
@implementation MessageLabel
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"keyPath:%@,\n object:%@,\n change:%@,\n context:%@",keyPath,object,change,context);
}
@end
第三 设置键的依赖关系
设置键之间的依赖关系,有两种方式,先看第一种实现方式,代码如下:
+(NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
if ([key isEqualToString:@"label"]) {
return [NSSet setWithObjects:@"model.name", @"model.age", nil];
}
return [super keyPathsForValuesAffectingValueForKey:key];
}
当添加一个被观察的keyPath,这个方法就会走。其大概意思是就是:用一组键的值去影响另一个键值。而上面的代码的意思就是,当添加的keyPath是label,那么将model.name和model.age的键作为键label的依赖,那么当model.name或model.age对应的值中任何一个发生改变,都会走MessageLabel中观察者的回调方法,就相当于直接改变label的值,也会走MessageLabel中观察者的回调方法一样。
注意 该方法一定要考虑非指定key时的情况要调用
[super keyPathsForValuesAffectingValueForKey:key],不能影响其他的情况
验证下,在touchBegin:方法中添加如下两行代码
self.model.name = @"逻辑";
self.model.age = 18;
运行点击屏幕触发touchbegin:方法,得到的打印结果如下
2019-01-25 22:45:38.746935+0800 KVOtest[2997:1087322] keyPath:label,
object:,
change:{
kind = 1;
new = ">";
},
context:(null)
2019-01-25 22:45:40.994048+0800 KVOtest[2997:1087322] keyPath:label,
object:,
change:{
kind = 1;
new = ">";
},
context:(null)
可以看到走了两次回调方法,是达到了预期结果。不过看不到label的改变,因为添加了依赖键,但是值并没有关联。这里就需要实现label的getter方法了:
-(MessageLabel *)label{
_label.text = [NSString stringWithFormat:@"姓名:%@,年龄:%ld",self.model.name,self.model.age];
return _label;
}
再运行,触发touchBegin方法,打印如下:
2019-01-25 23:05:00.652017+0800 KVOtest[3271:1096866] keyPath:label,
object:,
change:{
kind = 1;
new = ">";
},
context:(null)
2019-01-25 23:05:00.652332+0800 KVOtest[3271:1096866] keyPath:label,
object:,
change:{
kind = 1;
new = ">";
},
context:(null)
可以看到每次model.name 和model.age的改变都会使label的text属性值也会发生改变。
其实到现在,就完成了依赖键KVO。每次model的name或者age的值改变时,视图层label的text也会跟着变化,不需要每次为label的text赋值。
上面说了有两种设置依赖关系的方式,那现在说说这个第二种方式。
其实也是实现一个方法,只不过这个方法更有针对性,如下代码:
+(NSSet *)keyPathsForValuesAffectingLabel{
return [NSSet setWithObjects:@"model.name", @"model.age", nil];
}
可以看到,这个方法是专门针对label属性设置依赖关系的。其实每个属性都有一个这样的方法,当给类添加属性时,都会自动生成一个对应的方法。这种实现看起来更简洁易懂,效果是一样的。
总结一下依赖键KVO如下
- 调用添加观察者的方法;
- 调用删除观察者方法;
- 实现观察者回调方法;
- 实现keyPathsForValuesAffecting
方法设置依赖键; - 实现key的getter方法,并将依赖键值与key对应的值关联。
结语
到此KVO使用介绍完毕,文章中有些地方是为了方便自己理解,表述上不太规范,如有不妥或错误处欢迎你的指出,谢谢!