本文由快学吧个人写作,以任何形式转载请表明原文出处。
一、资料准备
二、基本信息
要了解KVO,必须了解KVC,KVO是建立在KVC的基础上的一套事件通知机制。
英文全称 :
Key-Value Observing
中文全称 : 键值观察
作用 : 添加KVO观察的对象的属性发生更改的时候,会发出通知。
使用范围 : 继承于NSObject的类。
KVO经常和NSNotificatioCenter做比较,简单说下 :
相同点 :
- 都是观察者,都可以监听事件。
- 对于通知来说,都是可以一对多的通知。
不同点 :
- KVO监听的是对象的属性(不是成员变量),具体来说就是遵循KVC规则的对象属性。NSNotificatioCenter监听的范围更广。
- KVO可以记录属性的旧值和新值的变化。
- 对于被观察者和观察者来说,KVO是1对1的,NSNotificatioCenter是1对多的。
- KVO使用后必须销毁,不然还会继续发送通知,就可能会出现野指针的情况。NSNotificatioCenter则在iOS9以后就不会向已经销毁的监听器发送通知了,也不会对已经销毁的被监听对象发送消息。所以就不会出现野指针。
三、KVO使用
1. 基本使用
- 注册观察者
- 响应回调函数
- 移除观察者
1. 注册观察者
objectivec
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
例如 :
objectivec
// 假设self 是一个vc,拥有属性man
[self.man addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
这就是说给self.man添加一个观察者是self,观察的是self.man的name属性。
参数 :
- 调用者(就是上面的self.man)就是被观察者。
- observer就是观察者
- keyPath就是要观察被观察者的哪个属性。
- options是观察策略.
- context是通过回调方法传递给观察者的任意数据。
对于options :
是个枚举值,有4个枚举元素 :
- NSKeyValueObservingOptionNew : 在回调方法里提供更改前的值
- NSKeyValueObservingOptionOld : 在回调方法理提供更改后的值
- NSKeyValueObservingOptionInitial : 观察最初的值,使用这个的话会在注册通知的时候(也就是上面的addObserver)就调用一次回调方法。
- NSKeyValueObservingOptionPrior : 在值修改前和修改后都调用回调方法。也就是调用两次回调方法。
对于context :
本身是一个函数指针类型,主要是用来区分被多个观察者的相同属性名,比如JDMan有name属性,JDKid也有name属性,都找了同一个观察者,用context来区分JDMan和JDKid的name属性,而不用if嵌套,可以使预编译和编译期的效率更高。
2. 回调方法
objectivec
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
观察者必须实现回调方法,通知也是从这里拿到的。一旦观察的属性发生了变化,就会进入到这个回调方法。
举例 :
less
#import "ViewController.h"
#import "JDMan.h"
static void*ManNameContext = &ManNameContext;
@interface ViewController ()
@property (nonatomic,strong) JDMan *man;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.man = [[JDMan alloc] init];
[self.man addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:ManNameContext];
// Do any additional setup after loading the view.
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == ManNameContext) {
NSLog(@"这是JDMan的name");
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
@end
参数 :
- keyPath : 被观察者的属性
- object : 被观察者
- change : 发生的变化
- context : 就是注册时候传递过来的,用来高效区分是哪个被观察者的属性。
3. 移除观察者
objectivec
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
举例 :
在上面举例的VC中实现dealloc
objectivec
- (void)dealloc
{
[self.man removeObserver:self forKeyPath:@"name"];
}
良好的习惯就是注册了就要记得在dealloc中移除。不然可能会出现野指针的问题。
2. 手动与自动发送通知
objectivec
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
要添加在被观察者的实现中。key就是被观察者的属性名称。默认返回时YES,也就是默认情况下,只要key这个名称的属性值发生了改变,就会自动调用回调函数。
如果给它返回值重写成NO。则不会调用回调函数。也就是不会发送通知。
例如 :
JDMan :
注意右边的return NO。
VC :
运行,点击button给self.man.name修改值,是不会调用回调方法的。设置成YES就可以了。如果只想监听某个属性,并且有时想监听,有时候又不想,那么就设置成NO。然后在需要监听的属性的setter方法里面加入如下代码 :
objectivec
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
再运行的时候,就会监听name的改变了。不需要回调的时候,直接去掉这两个方法就行。
3. 属性存在依赖
某一个属性的改变需要依赖其他的属性。
举例如下 :
三个属性存在关系,当haveDoneThings
或者allThings
发生改变的时候,doneRate
也要发生改变,存在的关系是: haveDoneThings/allThings = doneRate
。
需要被观察的是doneRate
,所以给doneRate
添加观察者为VC。并要在doneRate
的getter方法里面写明其关系 : haveDoneThings/allThings = doneRate
。
为了实时观察的到haveDoneThings
和allThings
改变后doneRate
的值。需要为doneRate
在类的实现文件中实现haveDoneThings
和allThings
与doneRate
的关联。
JDMan.m代码如下 :
VC代码如下 :
运行结果 :
不断的点击button,会看到doneRate
的变化 :
4. 观察可变数组
要注意的是给数组添加或者删除元素不要用[arr addObject:]这一类的方法,这种方法不是KVC的键值设置方式,所以不会走KVC的设值。而KVO是基于KVC进行观察的,所以是不能观察到可变数组通过原始方式添加或者删除值的。如下,是不会有观察回调的。
JDMan :
VC :
以
css
[[self.man mutableArrayValueForKey:@"mutArr"] addObject:@"1"];
设置可变数组的值,可以进行回调处理。
对于控制台输出的kind = 2 :
kind是NSKeyValueChange
枚举值 :
objectivec
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, //设置值
NSKeyValueChangeInsertion = 2, //插入值
NSKeyValueChangeRemoval = 3, //移除值
NSKeyValueChangeReplacement = 4, //替换值
};