上节研究完KVC后,随之关联的还有一个KVO,本篇就让我们来分析一下KVO的使用以及原理
一、KVO使用
- KVO通常的使用方法是
addObserver forKeyPath
再使用回调函数
处理结果
最后再dealloc
:移除掉观察者
对于添加观察对象方法:addObserver
,可以根据官方文档KVO官方查询相关用法,其中
-
addObserver
:观察对象:观察对象首先通过发送广告向被观察对象注册 -
keypath
:需要观察的值,只能是被观察对象的属性值 -
options
:参数指定为按位或选项常量:即是被观察属性的变化,来影响生成通知的方式。有下面几种方式
NSKeyValueObservingOptionNew:观察属性的新值
NSKeyValueObservingOptionOld:选择从更改之前接收观察到的属性的值
NSKeyValueObservingOptionInitial:可以使用此附加的一次性通知在观察者中建立属性的初始值
NSKeyValueObservingOptionPrior:可以指示观察到的对象在属性更改之前发送通知(除了更改之后的常规通知)。变更字典表示变更前通知,方法是将键NSKeyValueChangeNotificationIsPriorKey与包装为YES的NSNumber的值包含在一起
-
context
:上下文,是标记不同对象或者不同属性的作用。因为同一个文件里可能有多个被观察对象,或者一个观察对象有多个属性值被观察,使用 静态变量的地址(ex:static void *PersonNickContext = &PersonNickContext;
形式来分辨不同的对象或不同的属性。方便在回调函数中确定对象或者属性来进行后续操作。增加了代码的可读性,可扩展性,安全性 -
addObserver: forKeyPath :options:context:
方法不维护对观察对象、观察对象或上下文的强引用,因为是存在弱引用表中 -
dealloc
:每次调用addObserver后,都要调用dealloc
方法,在其中实现
移除
键值观察器消息
,指定观察对象
、路径
和上下文
:(ex: [self.personremoveObserver
:selfforKeyPath
:@"nick" context:NULL]);
注:
1、如果没有注册观察员,则请求将其删除为观察员会导致NSRangeException
2、释放时,观察者不会自动删除自身。被观察对象继续发送通知,而不考虑观察者的状态。但是,与任何其他消息一样,发送到已发布对象的更改通知会触发内存访问异常。因此,你要确保观察者在从内存中消失之前将自己移除。
3、没有提供询问对象是观察者还是被观察者的方法。构造代码以避免相关错误。一种典型的模式是在观察者初始化期间(例如在init或viewdiload中)注册为观察者,并在释放期间注销(通常在dealoc中),确保正确配对和有序地添加和删除消息,并且在将观察者从内存中释放之前取消注册。
-是否可以 不移除
:不可以。否则会崩溃,观察对象没被移除,但是观察者已经被释放了,再次注册时,添加观察器,消息发送后,系统不知道应该由哪个观察器接受。造成指针混乱(
由于第一次注册KVO观察者后没有移除,再次进入界面,会导致第二次注册KVO观察者,导致KVO观察的重复注册,而且第一次的通知对象还在内存中,没有进行释放,此时接收到属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,所以导致了类似野指针的崩溃,即一直保持着一个野通知,且一直在监听)
-
自动/手动接受消息
默认观察器都是自动接受到属性值变化的消息的。如果想要手动调用,则要关闭自动开关automaticallyNotifiesObserversForKey
并且在属性的set
方法中实现willChangeValueForKey
和didChangeValueForKey
,并在其中间赋值
当打开
自动开关( 默认)时,猜测系统检测属性值变化
,也是调用了willChangeValueForKey
和didChangeValueForKey
,
测试:
1、首先在addObserve
处打断点,使用watchpoint set variable self->_person->_nick
来观察属性值nick
(注:watchpoint set variable
:观察变量值改变命令)
2、然后点击页面,捕捉到nick
的变化
3、最终调入如下,堆栈显示
结论:属性值变化时,确实是willChangeValueForKey
和didChangeValueForKey
之间捕捉了nick
的set
方法 -
覆写keypath
:通过覆写keypath来定一个新的观察路径。
使用案例:
检测进度:定义一个进度属性:downloadProgress
以及相关属性:
覆写keypath
设置初始值
注册观察器
检测keypath变化
-
检测
数组
变化
定义一个可变数组:dateArray,点击页面时赋值,发现,在回调方法不走。这是为什么呢?
找到文档里关于观察数组时的要求:观察数组要按照kcv的形式赋值,才能发送更改消息
那么将数组按照kvc形式赋值,更改
结果收到了更改的消息,且类型kind为2,是NSKeyValueChange
的值,查到NSKeyValueChange
定义,有如下四种值的改变方式
二、KVO底层
都知道,KVO只能
观察属性
,不能观察成员变量,这个也有在代码里验证过,只能是属性可以被观察,这是为什么呢,属性和成员变量的区别就在于,多了set
和get
方法。说明,是kvo
只能观察set方法,捕捉到了值的变化,下面让我们来验证
- 根据文档,被观察对象的isa会指向一个中间类
这个中间类是什么,又是在什么时节生成的?
观察生成中间类时机,测试一下:
果然,调用完addObserver
后,生成了一个中间类,也可以叫做派生类NSKVONotifying_LGPerson
- 那么这个派生类里有哪些方法呢,
添加打印方法代码
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i
顺便添加一下打印类名的方法
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类以及它子类的名字
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i
调用
发现派生类实际上是当前类的子类
,且重新生成
了set方法
注:为什么方法
不
是继承
:因为继承的话,不会在子类中显示,只在父类中,这一点也可以添加LGPerson
类的一个子类
再打印一次方法列表试试,对比结果,也可以印证这一点。
- 何时
isa
再指回原类,猜测是在dealloc
里,移步到dealloc
测试:
确实调用完removeObserver
后,isa
再指回原类了。
- ️:派生类已经移除了么?
我们到dealloc
里处理打断点,移除观察者后,再调用获取类和子类的方法,发现派生类还存在。ps:测试页面销毁后,再次获取person类及其子类,还是同样的结果。
派生类根本就不移除了:因为
KVO派生类
只要生成,就会一直存在
,这样可以减少频繁的添加
操作
至此,整个KVO原理大致流程明白了:创建派生类实现了键值观察。
添加:addObserver
时,创建了派生类
,派生类是当前类的子类
,重写
了被监听属性的setter
方法,并将当前类的isa
指向了派生类
。(此时开始,所有调用本类的方法,都是调用的派生类
。派生类中没有的方法,就会沿着继承链查询到本类)
改变属性值: 派生类重写了被监听属性的setter
方法,在派生类的setter
方法触发时:在willChange之后
,didChange之前
,调用父类
属性setter
方法,完成父类属性的赋值。
移除: 在removeObserver
后,isa
从派生类
指回本类
。 但创建过的派生类
,不
会被本类从子类列表中移除
,会一直存在。
假象: 外部
打印class
永远看不到派生类
,是因为派生类将class方法重写了,故意不让外界看到。