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

相关推荐
一隅论数智5 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
XiHongShi20165 天前
STM32F407 RTC定时器例程,建议保存
stm32·单片机·学习
爱吃苹果的日记本5 天前
离散数学第六课
学习·离散数学
AI职业加油站5 天前
AI智能体应用工程师证书:政策红利下的职业新风口
大数据·运维·人工智能·学习·职场发展
旖旎夜光5 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
奇思妙想聪明勤奋的小羊5 天前
DeepAgents第5章:子Agent 与上下文隔离—让 Agent学会委派
人工智能·python·学习·语言模型
霍霍的袁5 天前
【C++】map 和 set 的使用 | 从用法到底层
开发语言·c++·学习·visual studio
彧azz5 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
我爱cope5 天前
【操作系统 | 计算机硬件:CPU、内存与 I/O 如何支撑操作系统?】
学习·操作系统