前言
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
个Method
setName:
class
dealloc
_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
。