前言
iOS 底层第20天的学习。今天分享的内容是 KVO 一些你从未涉及到的东西。
What is KVO ?
- 查看 苹果官方文档

- KVO :
Key-Value Observing是一种机制,当其他对象的特定属性发送变化时会通知某个对象
KVO 如何使用

- 使用
KVO,首先你必须确定被观察的对象是否支持。如果你的对象继承NSObject且通过常规的方式创建属性。你的对象和属性会自动支持KVO,也可以手动支持。 - 其次,你需要把观察者
Person注册到被观察者的Account上。Person发送一个addObserver:forKeyPath:options:context:消息给Account


- 为了能够接受到来自
Account的消息,Person观察者需要实现一个接口observeValueForKeyPath:ofObject:change:context,Account将会发送一个消息给Person在任何时间点keyPath发生变化。Person也能根据通知的变化采取相应的措施。


- 最后,当不再需要通知时,至少在观察者
deallocated之前通过Person对象移除册从Account发送消息removeObserver:forKeyPath:

- 小结:
kvo三部曲- 第一步:
receive observer - 第二步:
observed property of an object changes - 第三步:
remove observer
- 第一步:
第一步:receive observer
[self.xk_student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
第二步: observed property of an object changes
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"change %@",change);
}
第三步:remove observer
- (void)dealloc{
[self.xk_student removeObserver:self forKeyPath:@"name"];
}
- 给
name进行赋值
self.xk_student.name = @"emiya";
- 打印输出👇

addObserver 参数分析
Options
- 可传入
4个类型,分别是👇
| 值 | 功能 |
|---|---|
NSKeyValueObservingOptionNew |
作为变更信息的一部分发送新值 |
NSKeyValueObservingOptionOld |
作为变更信息的一部分发送旧值 |
NSKeyValueObservingOptionInitial |
在注册时发送一个初始更新 |
NSKeyValueObservingOptionPrior |
在变更前后分别发送变更,而不只在变更后发送一次 |
Context
- 先来看一下官方的解释 👇

大致的意思就是:
context pointer在更改通知中传递回观察者的任意数据,您可以指定NULL和key path来确定更改通知的来源,但是这种方法可能会导致对象出现问题,因为其超类出于不同的原因也会观察同一个key path。
一种更安全、更可扩展的方法是使用Context来确保您收到的通知是针对您的观察者而不是超类的。
- 写个🌰 看一下
self.xk_student = [XKStudent new];
self.xku_student = [XKUniversityStudent new];
// XKUniversityStudent 继承 XKStudent
// 给 xk_student 和 xku_student 同事注册对 name 的监听
[self.xk_student addObserver:self forKeyPath:@"name"
options:(NSKeyValueObservingOptionNew) context:XKStudentContext];
[self.xku_student addObserver:self forKeyPath:@"name"
options:(NSKeyValueObservingOptionNew) context:XKUniversityStudentContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if(context == XKStudentContext) {
NSLog(@"\n change %@ \n XKStudentContext",change);
}else if (context == XKUniversityStudentContext){
NSLog(@"\n change %@ \n XKUniversityStudentContext",change);
}
}
- 给 name 赋值
self.xk_student.name = @"emiya";
self.xku_student.name = @"un emiya";
- 打印输出结果

- 小结:
Context就是一个标识,为了就是当key path相同时用来区分子类和分类监听同一个属性
automatic for Observers
- 我们可以设置
automaticallyNotifiesObserversForKey返回NO
@implementation XKStudent
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
- 设置
No以后对属性的监听就失效了,你也可以用key进行判断,对某一个属性进行设置。
mutableArray for Observe
- 我们再试一下如何对可变数组对象进行监听
// 可变数组观察,先观察再变值
self.xk_student.dataArray = [NSMutableArray array];
[self.xk_student addObserver:self forKeyPath:@"dataArray"
options:(NSKeyValueObservingOptionNew) context:NULL];
[[self.xk_student mutableArrayValueForKey:@"dataArray"] addObject:@"数组元素1"];
- 打印输出

- 这里有个坑点就是必须调用
mutableArrayValueForKey:@"dataArray"],不能使用self.xk_student.dataArray
KVO 底层原理
- KVO 底层实现详情

根据官方文档 👇
KVO的实现使用了一个叫做isa-swizzing的技术。而isa-swizzing可以理解为isa 指向的替换(如不清楚isa的可以看下这篇文章)isa指针指向一个维护分派表的对象类。这个分派表本质上包含指向类实现的方法的指针以及其他数据。- 当一个观察者为一个对象的属性注册时,被观察对象的
isa指针被修改,指向一个中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。- 你决不能依赖
isa指针来确定类的成员。相反,你应该使用class方法来确定对象实例的类。
- 说真心话技术这种官方文档翻译过来真心好难理解,因此还是需要用
技术的方式去验证。 - 进入
lldb进行调试

- 由输出可知 当 进行了
addObserver后isa->NSKVONotifying_XKStudent
这个
NSKVONotifying_XKStudent到底是什么呢?NSKVONotifying_XKStudent与XKStudent有何关系呢?
- 继续
lldb进行调试

- 由👆可知
XKStudent与NSKVONotifying_XKStudent是父子关系 - 继续调试分析
NSKVONotifying_XKStudent里面到底有什么呢? -
lldb分析👇

- 根据输出结果可知
NSKVONotifying_XKStudent有4个MethodsetName:classdealloc_isKOVA
那这
4个Method里面到底做了什么的?
_isKOVA其实就是一个标识。判断是不是KVO生成的。-
class进行分析,输出👇
你是否有疑问 isa -> NSKVONotifying_XKStudent ,为何不输出 NSKVONotifying_XKStudent。 其实 class 已经做了处理,对外部让我们开发人员看到的永远都是 XKStudent。可以简单理解为:做了一次掩饰。
-
dealloc字面理解就是销毁。
当我们在addObserver的时isa->NSKVONotifying_XKStudent。那何时把isa指回去呢?
对dealloc进行分析,输出👇


由输出可知, 当 removeObserver 的时 , isa -> XKStudent,故能推导出其实在 dealloc 时候又把 isa 指回了 XKStudent
-
setName:从字面理解就是setter方法
疑问1:假设
KVO对setter可以进行监听观察,那它是否也会对成员变量进行观察呢?
开始进行验证
// 添加成员变量
@interface XKStudent : NSObject{
@public
NSString *age;
}
// 添加观察
[self.xk_student addObserver:self forKeyPath:@"name"
options:(NSKeyValueObservingOptionNew) context:NULL];
[self.xk_student addObserver:self forKeyPath:@"age"
options:(NSKeyValueObservingOptionNew) context:NULL];
// 进行赋值
self.xk_student.name = @"emiya";
self.xk_student->age = @"20";
// 监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"\n change %@",change);
}
打印输出👇
change {
kind = 1;
new = emiya;
}
从输出结果可知:KVO 只会对属性 setter 进行监听,无法对成员变量进行监听。
疑问2:已知
isa -> NSKVONotifying_XKStudent,那么setName:肯定是NSKVONotifying_XKStudent对其进行了赋值操作。那如果isa->XKStudent那么name是否已经有值了?
开始进行验证

由输出可知:在 isa -> XKStudent 后,name 已经有值了 。
疑问3:可以肯定是在
KVO_XKStudent这一级进行了name的赋值,那在XKStudent里的name是何时进行赋值的呢?
开始在 lldb 里进行符号断点
watchpoint set variable self->_xk_student->_name
输出👇

bt 打印堆栈

由👆可知
-> 1.touche'sBegin 进行 KVO_XKStudent.setName:
-> 2.Foundation:_NSSetObjectValueAndNotify
-> 3.Foundation:-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
->4.Foundation:-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
进入 4.Foundation:-[NSObject(NSKeyValueObservingPrivate) 查看汇编

在最后 using block 把值返回

进入 2.Foundation:_NSSetObjectValueAndNotify 查看汇编

_implicitObservationInfo 会发送一个通知 ,这个通知就会来到 NSKeyValueDidChange
最后来到 _observeValueForKeyPath:ofObject:。

- 小结:解释动态子类
KVO_XKStudent setter到底做了什么。
其实就是调用了父类Student setter方法,然后在valueDidChange后发起一个通知说明值已经赋值完毕了,最后来到observeValueForKeyPath。
KVO 底层原理流程总结

