KVO学习

#【iOS】 KVO学习

文章目录

前言

  • KVO全称KeyValueObserving,俗称键值监听,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
  • KVC和KVO都属于键值编程而且底层实现机制都是isa-swizzing
  • KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
  • KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。

KVO的使用

使用方法

  1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
  2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
  3. 当观察者不需要监听时,可以调用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;
}

每个部分的具体解释

  1. **Observer:**它是由谁来监视这个对象
  2. **keyPath:**它是指监视这个对象的哪个属性
  3. **options:**这个是指定你希望系统告诉你什么常见的有NSKeyValueObservingOptionOld旧值NSKeyValueObservingOptionNew新值,NSKeyValueObservingOptionInitial添加观察时就立刻出发一次回调
  4. **change:**这个是回调时显示的改变,除了包含上面所说的options所选的那些之外,它还会包含一个kind,表示你这次是什么改变,

值为1时表示属性被重新赋值,值为2时表示插入,用于有序集合,值为3时表示删除操作,也是用于有序集合

  1. 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,所以willChangedidChange必然会执行,观察者自然能收到通知。

相关推荐
方白羽1 小时前
为什么 Android 非要用 Intent 传值?
android·ios·harmonyos
辣知2 小时前
辣知·化智47 黄帝陵与民族精神归途
学习
知识分享小能手2 小时前
深度学习学习教程,从入门到精通,数值计算 — 知识点详解(4)
人工智能·深度学习·学习
zyf1044163 小时前
暑期实践日志 Day49:复盘题库出题成果,夯实章节考点
学习·计算机网络·剪辑·暑期实践·课题任务
dora3 小时前
iOS开发新手的第一行代码
ios
kaixin_啊啊3 小时前
多模态学习
学习
m4Rk_4 小时前
【论文阅读】Agent 记忆机制(62):DCM-Agent——用双簇记忆化解优化问题的多范式冲突
论文阅读·人工智能·学习·开源·github
xian_wwq4 小时前
【学习笔记】深度认知系列-第13讲AI Agent时代到来——从“回答问题”到“执行任务”
人工智能·笔记·学习
程序猿炎义4 小时前
【llm-algo-leetcode学习笔记】量化优化手段
笔记·学习