这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战
1. KVO
KVO(Key Value Observing, 键值观察)
是Objective-C对观察者模式的实现,每次当被观察对象的某个属性值发生改变
时,注册的观察者便能获得通知
。
1.1注册一个kvo
observer:
观察者 也就是被观察对象发生改变时通知的接收者keyPath
:被观察的属性名options
:参数 这里一般选择NSKeyValueObservingOptionNew
,NSKeyValueObservingOptionOld
。也就是在回调方法里会受到被观察属性的旧值和新值,默认为只接收新值。如果想在注册观察者后,立即
接收一次回调,则可以加入NSKeyValueObservingOptionInitial
枚举。context:
这个参数可以传入任意类型的对象
,这个值会传递到接收消息回调的代码中,是KVO中的一种传值
方式,可以当作标识符
用来区分观察到的属性。
1.2移除一个kvo
注意
- 移除观察者的时候如果他还没注册,那么就会报NSRangeException。注意要保持注册和移除的一对一关系,如果无法确定的话,就把removeObserver 放在try catch里面执行。
- kvo在dealloc的时候不会自动移除,被观察的对象会一直给观察者发消息,即使观察者已经被释放了。如果给被释放的观察者发消息,那么就会有内存访问的错误。所以要在释放观察者之前要移除kvo。
- protocols 没有提供询问物体是观察者还是被观察者的方法。为了避免释放的错误,一个典型的模式是在初始化的时候(例如在init或viewDidLoad中)注册为观察器,并在dealloc的时候移除观察者。要确保一个注册对应一个移除,并且确保观察者在被释放之前移除。
当这个对象没有移除观察者的时候,那么当self被释放
之后,那么student就还会继续给self发送消息
,这样就会导致崩溃
。
1.3手动和自动 kvo
automaticallyNotifiesObservers
是控制手动和自动kvo
的地方,当返回YES
时候就是自动
,返回NO
就是手动
。
当返回NO的时候,需要在赋值之前调用willChangeValueForKey
,赋值之后调用didChangeValueForKey
,这样才会触发回调。
1.4 一对多
当要观察的属性被多个属性影响的时候,那么就可以调用setByAddingObjectsFromArray
。
1.5 可变数组观察
当观察可变数组的时候,这样的方法添加的object,是无法被kvo观察到的。
如果想要被kvo
观察到,则需要mutableArrayValueForKey
这个方法去添加。
添加后看到kind是2。
看到其对应的是Insertion
,也就是插入。
2. KVO 原理
KVO
是使用一种称为isa-swizzing
的技术实现的,当观察者为一个对象的属性注册时,被观察对象的isa指针
被修改,指向一个中间类
而不是真正的类。
在注册的地方打个断点后运行。
输出person的class发现是LGPerson
,往下走也就是注册了之后在重新输出,发现是一个叫NSKVONotifying_LGPerson
的类。
所以可以知道,注册观察者的时候系统自动生成了一个NSKVONotifying_LGPerson
类。为了证明是生成的而不是原先就存在的。重新运行,打下断点在注册观察者之前,发现NSKVONotifying_LGPerson
是不存在
的,所以NSKVONotifying_LGPerson是动态生成的
。
那么NSKVONotifying_LGPerson
和LGPerson
有什么关系呢?
用下列方法在注册观察者前后打印LGPerson类的子类,
运行后看到输出,发现NSKVONotifying_LGPerson是LGPerson的子类
,而NSKVONotifying_LGPerson没有子类
。
2.1 NSKVONotifying_LGPerson
那么NSKVONotifying_LGPerson
里面有什么东西呢?
用下面的方法打印出NSKVONotifying_LGPerson的方法列表
。
运行后发现,NSKVONotifying_LGPerson的方法有4个:
setNickName
:setter
class
:类型
dealloc
:释放实例对象,isa重新指回去。
_isKVOA
:是否KVO标识符。
这四个方法都是NSKVONotifying_LGPerson重写
的而不是继承来的,可以打印LGStudent的方法列表来证明一下。发现什么都没有,说明如果是父类的就不会打印,所以NSKVONotifying_LGPerson里的方法都是重写的。
2.2 isa 指向
看到NSKVONotifying_LGPerson有dealloc
,那么NSKVONotifying_LGPerson是否会释放呢?NSKVONotifying_LGPerson释放之后isa是否会指回LGPerson呢?
在dealloc
里面移除观察者然后打下断点
后运行。
这里输出object_getClass(self.person)
,发现是NSKVONotifying_LGPerson
,往下走之后重新输出object_getClass(self.person)
,发现输出结果成了LGPerson
。这就代表着,isa重新指回了LGPerson
。
回到之前的viewController,然后点击屏幕触发之前写好的方法,看到还是有输出NSKVONotifying_LGPerson类,代表着NSKVONotifying_LGPerson还存在,没有被销毁
。
2.3 class 方法
重新进来,在添加观察者处后打下断点。
在lldb里面输出 self.person.class。按照正常的逻辑,现在self.person.class 输出的应该是NSKVONotifying_LGPerson,但是输出后发现是LGPerson
。这里是因为NSKVONotifying_LGPerson只是用来帮忙做一些事情的,而明面上显示出来的还是LGPerson。
2.4 setter 方法
setter 方法是KVO
的主要方法。setter存在,那么就排除了观察的是属性而不是成员变量
,因为成员变量是没有setter的。
为成员变量添加观察者后运行。
点击屏幕,发现没有观察到Cooci,说明KVO是不观察成员变量的,同时也是对setter方法进行了监听
。
那么这里调用的是谁的setter方法呢?
当把观察者移除后,发现还是可以调用setter方法,说明调用的是LGPerson的setter。
运行后再lldb为self->_person->_nickName打下断点。
往下走,触发setter方法后,发现进入了汇编。
lldb输出bt,发现确实调用了LGPerson 的setter
,并且中间还调用了_NSSetObjectValueAndNotify
,_changeValueForKey
。setNickName之所以被调用,就是因为中间的这些方法的影响。那么中间这些方法做了什么呢?
查看_NSSetObjectValueAndNotify
方法,看到这里调用了willChangeValueForKey
和didChangeValueForKey
。
往下看看到一个block的回调。
在看_changeValueForKey
里面。发现里面做了键值观察
,并且调用了NSKeyValueWillChange
和NSKeyValueDidChange
。
在observeValueForKeyPath
回调处打下断点并触发回调,发现确实是从NSKeyValueDidChange
里面来的,并且在NSKeyValueDidChange里面发送了一个通知。
3. 总结
- 要保持注册和移除的一对一关系
- 释放观察者之前要移除kvo。
- automaticallyNotifiesObservers是控制手动和自动kvo的地方,当返回YES时候就是自动,返回NO就是手动。当返回NO的时候,需要在赋值之前调用willChangeValueForKey,赋值之后调用didChangeValueForKey,这样才会触发回调。
- 当要观察的属性被多个属性影响的时候,那么就可以调用setByAddingObjectsFromArray。
- 当观察可变数组的时候,需要mutableArrayValueForKey这个方法去添加。
- KVO是使用一种称为isa-swizzing的技术实现的,当观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向一个中间类而不是真正的类。
- 注册观察者的时候系统动态生成了一个NSKVONotifying_XXXXXX类,是XXXXXX类的子类
- NSKVONotifying_XXXXXX类重写了class,dealloc,_isKVOA和setter方法
- NSKVONotifying_XXXXXX类dealloc之后isa重新指回了LGPerson,NSKVONotifying_LGPerson还存在,没有被销毁。
- KVO观察的是属性而不是成员变量