#【iOS】 KVO学习
文章目录
-
- 前言
- KVO的使用
- 手动KVO
- KVO本质原理
- 面试题
-
- iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 如何手动触发KVO?
- 直接修改成员变量会触发KVO么?
- [KVC 与 KVO 的不同?](#KVC 与 KVO 的不同?)
- [KVO和 notification(通知)的区别?](#KVO和 notification(通知)的区别?)
- [KVO与 delegate 的不同?](#KVO与 delegate 的不同?)
- 为什么利用KVC修改会触发KVO
前言
- KVO全称KeyValueObserving,俗称键值监听,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
- KVC和KVO都属于键值编程而且底层实现机制都是isa-swizzing。
- KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
- KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。
KVO的使用
使用方法
- 通过
addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。 - 在观察者中实现
observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。 - 当观察者不需要监听时,可以调用
removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。
使用实例
我们从一个最简单的KVO说起
objc
- (void)viewDidLoad {
[super viewDidLoad];
Student* student = [Student new];
[student addObserver: self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
student.age = 20;
student.age = 19;
[student removeObserver:self forKeyPath:@"age"];
student.age = 20;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"keyPath = %@", keyPath);
NSLog(@"object = %@", object);
NSLog(@"change = %@", change);
}
输出结果:
objc
keyPath = age
object = <Student: 0x600003074520>
change = {
kind = 1;
new = 20;
old = 0;
}
keyPath = age
object = <Student: 0x600003074520>
change = {
kind = 1;
new = 19;
old = 20;
}
每个部分的具体解释
- **Observer:**它是由谁来监视这个对象
- **keyPath:**它是指监视这个对象的哪个属性
- **options:**这个是指定你希望系统告诉你什么常见的有
NSKeyValueObservingOptionOld旧值NSKeyValueObservingOptionNew新值,NSKeyValueObservingOptionInitial添加观察时就立刻出发一次回调 - **change:**这个是回调时显示的改变,除了包含上面所说的options所选的那些之外,它还会包含一个kind,表示你这次是什么改变,
值为1时表示属性被重新赋值,值为2时表示插入,用于有序集合,值为3时表示删除操作,也是用于有序集合
- Context:是给你区分不同 KVO 观察的
手动KVO
默认情况下,KVO 是自动触发的。
如果想要手动KVO,就需要关闭自动通知
objc
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
然后用willChangeValueForKey: ,didChangeValueForKey:来自己控制
例如:
objc
- (void)setAge:(NSInteger)age {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
KVO本质原理
objc
- (void)viewDidLoad {
[super viewDidLoad];
Student* student = [Student new];
Student* student2 = [Student new];
[student addObserver: self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
student.age = 20;
student2.age = 19;
[student removeObserver:self forKeyPath:@"age"];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"keyPath = %@", keyPath);
NSLog(@"object = %@", object);
NSLog(@"change = %@", change);
}
//打印结果
keyPath = age
object = <Student: 0x6000039e0ae0>
change = {
kind = 1;
new = 20;
old = 0;
}
在这段代码里,我只监听了student,而打印结果也确实只监听了student。 但我们知道给属性赋值其实在底层调用的都是类里的实例方法setName:方法,根据iOS的机制,应该都是按照Student的isa指针,去类对象中查找方法,怎么就能做到一个监听一个不监听呢?
分析
针对上面的疑问,我们猜测是不是两个对象的isa指针不一样呢,不都是指向Student这个类的
查询发现添加了监听的student的isa指针指向的是NSKVONotifying_Student,而student2指向的才是NSKVONotifying_Student
也就是说student在添加监听后它的类对象就改变了
原理
利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数 willChangeValueForKey: 父类原来的setter didChangeValueForKey: 内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
简单来说也就是子类重写了set方法,逻辑类似于:
objc
- (void)setAge:(NSInteger)age {
// 通知观察者:马上要变了
[self willChangeValueForKey:@"age"];
// 调用父类 setter
[super setAge:age];
// 通知观察者:已经变了
[self didChangeValueForKey:@"age"];
}
面试题
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数 willChangeValueForKey: 父类原来的setter didChangeValueForKey: 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
如何手动触发KVO?
- 手动调用willChangeValueForKey:和didChangeValueForKey:
直接修改成员变量会触发KVO么?
- 不会触发KVO
因为,触发KVO是因为,执行set方法时候,调用 willChangeValueForKey didChangeValueForKey 但是直接修改成员变量不会调用set方法
KVC 与 KVO 的不同?
KVC(键值编码),即 Key-Value Coding,一个非正式的 Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用 Setter、Getter 方法等显式的存取方式去访问。 KVO(键值监听),即 Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了 setter 方法、或者使用了 KVC 赋值。
KVO和 notification(通知)的区别?
notification 比 KVO 多了发送通知的一步。 两者都是一对多,但是对象之间直接的交互,notification 明显得多,需要notificationCenter 来做为中间交互。而 KVO 如我们介绍的,设置观察者->处理属性变化,至于中间通知这一环,则隐秘多了,只留一句"交由系统通知",具体的可参照以上实现过程的剖析。
notification 的优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,例如键盘、前后台等系统通知的使用也更显灵活方便。 (参照通知机制第五节系统通知名称内容)
KVO与 delegate 的不同?
和 delegate 一样,KVO 和 NSNotification 的作用都是类与类之间的通信。但是与 delegate 不同的是: 这两个都是负责发送接收通知,剩下的事情由系统处理,所以不用返回值;而 delegate 则需要通信的对象通过变量(代理)联系; delegate 一般是一对一,而这两个可以一对多
为什么利用KVC修改会触发KVO
KVO(键值观察)的底层实现是ISA指针交换 (动态生成子类),系统会在运行时重写被观察属性的setter方法。在这个重写的setter里,系统插入了willChangeValueForKey:和didChangeValueForKey:这两个通知方法。
而KVC(键值编码)在修改属性时,默认的调用路径 是:
setValue:forKey: -> 优先查找 set<Key>: 方法 -> 如果找到了,就直接调用这个setter。
因为KVC最终调用的是被KVO交换过的setter,所以willChange和didChange必然会执行,观察者自然能收到通知。