介绍
KVO:key-value-observing,键值监听机制,由NSKeyValueObserving协议提供支持,NSObject类继承了该协议,所以NSObject的子类都可使用该方法
当某个对象的属性值发生改变时,可以通过KVO监听
使用方法
- 注册观察者
给对象绑定一个监听器(观察者)
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
//addObserver:观察者,一般为self,例如控制器本身
//forKeyPath:要监听的属性
//options:选项(你想要拿到的属性值是新的还是旧的)
//context:一般设为nil
//options参数选择
NSKeyValueObservingOptionNew = 0x01
NSKeyValueObservingOptionOld = 0x02
- 实现回调方法
当监听的属性值发生改变时,监听器就会回调自身的监听方法,如下
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
//keyPath:要改变的属性
//object:要改变的属性所属的对象
//change:改变的内容
//context:上下文
- 触发回调方法
- 移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
[person removeObserver:self forKeyPath:@"name"];
完整示例
// Person.h
@property(nonatomic,copy) NSString *name;
// ViewController.m
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self kvoTest];
}
-(void)kvoTest{
Person *person = [[Person alloc]init];
//添加观察者
// [person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld context:nil];
//修改属性值
person.name = @"ZS";
//修改属性值
person.name = @"GCK";
//移除观察者
[person removeObserver:self forKeyPath:@"name"];
}
//实现回调方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@-----%@------%@",keyPath,object,change);
}
//打印结果,当选项是打印新值时
2021-03-30 16:26:26.876826+0800 KVO-test[10021:436508] name-----<Person: 0x60000039ccf0>------{
kind = 1;
new = ZS;
}
2021-03-30 16:26:26.877043+0800 KVO-test[10021:436508] name-----<Person: 0x60000039ccf0>------{
kind = 1;
new = GCK;
}
//打印结果,当选项是打印旧值时
2021-03-30 16:30:04.230746+0800 KVO-test[10092:440337] name-----<Person: 0x6000036ac700>------{
kind = 1;
old = "<null>";
}
2021-03-30 16:30:04.231015+0800 KVO-test[10092:440337] name-----<Person: 0x6000036ac700>------{
kind = 1;
old = ZS;
}
也可以通过这种方式打印新旧值
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
//打印结果
2021-03-30 16:32:43.094120+0800 KVO-test[10142:442921] name-----<Person: 0x600003d8c530>------{
kind = 1;
new = ZS;
old = "<null>";
}
2021-03-30 16:32:43.094395+0800 KVO-test[10142:442921] name-----<Person: 0x600003d8c530>------{
kind = 1;
new = GCK;
old = ZS;
}
总的来说就是当你监听的属性(name)发生改变时,就会通知监听者(self,控制器),执行监听者的observeValueForKeyPath
方法
应用场景
-
当数据模型的数据发生改变时,视图组件能动态的更新
-
监听scrollView的contentOffset属性,来完成用户滚动时动态改变某控件的属性实现效果,例如下拉刷新等
-
......
实现原理
问题
- iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 如何手动触发KVO?
KVO是基于Runtime机制实现的,当某个类的属性对象第一次被观察时,系统就会在运行期间动态地创建该类的一个派生类,这个派生类中重写基类中任何被观察属性的setter方法,派生类在被重写的setter方法内实现真正的通知机制
当一个类对象第一次被观察时,系统会将isa
指针指向动态生成的派生类,从而在给被监听属性赋值时执行的是派生类的setter
方法
举个例子:
通过断点,可以看到在addObserver
方法前,person属于Person类,执行完addObserver
方法后,person属于NSKVONotifying_Person
类了,也就是说一旦对象添加了KVO监听后,其isa指针会发生变化,因此set方法的执行效果也不一样了
NSKVONotifying_Person
其实是Person
的子类,NSKVONotifying_Person
是Runtime在运行时生成的
person
对象在调用setName
方法的时候,肯定会根据person
的isa
指针找到NSKVONotifying_Person
,在NSKVONotifying_Person
中找setName
的方法及实现
NSKVONotifying_Person
的setName
实现原理
- 首先调用了
willChangeValueForKey:
- 然后调用父类的
setName
方法对成员变量赋值 - 最后调用
didChangeValueForKey:
,这个方法会调用观察者的监听方法,最后来到观察者的observeValueForKeyPath
方法
PS:KVO的这套实现机制苹果还偷偷重写了class方法,让我们误以为使用的还是当前类,从而达到了隐藏生成的派生类
回答问题:
问题
- iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
答:当一个对象使用了KVO监听,系统会修改这个对象的 isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现
set方法内部会顺序调用
willChangeValueForKey
方法、原来的setter方法实现、didChangeValueForKey
方法,而didChangeValueForKey
方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:
监听方法。
- 如何手动触发KVO?
答:被监听的属性值被修改时,会自动触发KVO;如果想要手动触发KVO,则需要我们自己调用
willChangeValueForKey
方法和didChangeValueForKey
方法,这样就可以在不改变属性值的情况下手动触发KVO,两个方法缺一不可
参考文章