1.kvo简介
KVO全称KeyValueObserving,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。
KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。
看下官方文档
KVO是一套通知机制,这台机制让一个对象可以收到一个通知,这个通知描述的是另一个对象它的属性发生的变化。
2.kvo使用
运行:
这就是kvo的基本使用 官网上也有介绍
Person对象监听Account对象
1、注册观察者:addObserver:forKeyPath:options:context:
2、实现通知回调:observeValueForKeyPath:ofObject:change:context:
3、移除观察者: removeObserver:forKeyPath:
// observer: 观察谁,弱引用添加
// keyPath:观察什么
// options:观察什么的变化,枚举值 - 新值、旧值
// context:上下文 为了进行观察这区分,解决问题,可以在监听回调方法中减少判断,代码可读性会降低
context
官方:
addObserver:forKeyPath:options:context: message中的上下文指针包含将在相应的更改通知中传递回观察者的任意数据。您可以指定NULL并完全依赖于键路径字符串来确定更改通知的来源,但是这种方法可能会给其超类出于不同的原因也在观察相同键路径的对象造成问题。
一种更安全、更可扩展的方法是使用content来确保接收到的通知是发送给观察者的,而不是超类。
类中唯一命名的静态变量的地址可以作为一个良好的上下文(content)。在超类或子类中以类似方式选择的上下文将不太可能重叠。您可以为整个类选择一个上下文,并依赖通知消息中的关键路径字符串来确定更改的内容。或者,您可以为观察到的每个键路径创建不同的上下文,这样就完全不必进行字符串比较,从而提高通知解析的效率。
大意就是:你和你的父类可能同时监听了相同的key path;如果没有上下文context的支持无法做到区分。
这样就做出了区分
移除观察者的纯粹性和必要性
1、阐述了纯粹性:已除的肯定是之前已经添加过的观察者,如果未添加就会报NSRangeException异常,即不能重复添加
2、阐述了必要性:首先观察者不会自动的remove;被观察的对象还会向观察者发送通知,如果此时观察者已经released,那么就会出现内存访问异常
3、给出建议使用:在init或viewDidLoad中注册观察者addObserver;dealloc中移除观察者removeObserver
手动和自动
使用automaticallyNotifiesObserversForKey:方法可以实现对当前对象的某个属性的自动观察做开关处理。
设置为no时就不会响应 也就是通过automaticallyNotifiesObserversForKey类方法屏蔽某个属性的自动通知逻辑
然后手动实现下面方法就可以正常响应
路径处理
比如我们监听下载的进度时,可知下载的进度 = 已下载量 / 总下载量,可我们又不想监听两个量,这个时候可以使用keyPathsForValuesAffectingValueForKey方法进行观察处理。
运行后结果:
数组观察
输出发现没有变化 但是数组的确增加了
官方文档:
根据官方文档做如下处理,将修改数组的位置修改为
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, // 设置
NSKeyValueChangeInsertion = 2, // 插入
NSKeyValueChangeRemoval = 3, // 移除
NSKeyValueChangeReplacement = 4, // 替换
};
上方的2对应数组的addObject等操作,3对应数组的removeObject等操作,4对应数组的replaceObjectAtIndex:withObject:的操作
KVO底层原理探索
KVO观察属性
** 通过打印结果可知,KVO只对属性进行监听,对成员变量不监听. ** 属性与成员变量的区别在于属性存在 setter方法,而成员变量没有setter
中间类
self.person的isa的指向发生了变化
通过断点打印可知,在添加监听之后,self.person的isa重新指向了NSKVONotifying_LGPerson类,对比之前的类多了NSKVONotifying_前缀。
** 派生类 **:即某个类的子类
自定义方法,查看LGPerson的子类情况,代码如下:
并在self.person添加监听的前后进行调用该方法,
查看打印结果。
其中LGStudent是LGPerson的一个子类,在添加监听方法后发现LGPerson多了一个子类NSKVONotifying_LGPerson,说明添加监听方法后,self.person的isa指向了LGPerson的子类。
中间类中有什么
是否存在什么方法。
自定义代码打印类的方法。
调用方法
打印
从打印结果可知,在NSKVONotifying_LGPerson类中添加了四个方法,分别为:setNickName、class、dealloc、_isKVOA这四个方法。
_isKVOA 判断当前是否为KVO类
dealloc 释放
setNickName 需要查看是继承还是重写的,答案是重写的方法
class
那我们继续打印下另一个子类student的方法
子类student先不实现任何方法
打印没有
接下来实现一个setNickName
打印就有了setNickName
这个时候发现LGStudent中存在setNickName方法,由此可知NSKVONotifying_LGPerson中的setNickName为重写的方法
isa是否会还原
NSKVONotifying_LGPerson类是否会移除,self.person的isa是否会指回来。
在ViewController中的dealloc方法中添加断点,查看移除之后的self.personisa的指向。
通过打印可知,在移除监听后,self.person的isa会重新指向LGPerson
在pop的ViewController也做LGPerson的子类的打印,
结果如下:
发现当监听被移除后,NSKVONotifying_LGPerson类并没有被移除,而是仍然存在。
结论是 对象的isa会重新指向 动态生成的类会一直存在
重写的setter方法内做了什么处理
猜测一下,首先,在调用NSKVONotifying_LGPerson重写setter方法的时候,改变的是其父类LGPerson的nickName的值,那么在重写的setter方法中一定有对父类nickName进行传值的操作。
设置观察self->_pserson->_nickName,具体命令为:
watchpoint set variable self->_person->_nickName
执行命令后的结果。进入断点。
查看左侧堆栈,可知2、3、4步是隐藏逻辑为汇编语言
堆栈编号1为LGPerson的setter方法
堆栈编号2在断点前后分别执行NSKeyValueWillChange方法以及NSKeyValueDidChange方法。
结论:在willChange与didChange之间调用父类的赋值方法,因此,父类的值得以改变。