相信KVO对大家来说都不陌生,即使你没有使用过,基本也都曾了解过,听说过。
这篇文章作者就带大家来深入研究一下KVO的实现原理,以及使用,目的是让大家真正的理解KVO。
一. 了解方法
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
注册监听者
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
实现监听方法
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
注销监听
看到官方给我们听过的几个方法有没有觉得和通知、代理很类似?
其实就是一样的用法特别简单,共分为三个步骤:
- 注册
- 监听
- 移除
二. 注册监听者
我们来看注册监听方法
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
我们来看几个参数:
- observer: 监听对象(就是谁来监听KVO事件)
- keyPath: 监听属性的名字
- context: 添加监听方法的最后一个参数,是一个可选的参数,可以传任何数据,这个参数最后会被传到监听者的响应方法中,可以用来区分不同通知,也可以用来传值。如果你要用context来区分不同的监听知,一个推荐的做法是声明一个静态变量,其保持它自己的地址,这个变量没有什么意义,但是却能起到区分的作用,如下:
static void *ObservePersonContext = &ObservePersonContext;
- NSKeyValueObservingOptions: 监听选项是个枚举,我们来看一下都有那些类型可选
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
- NSKeyValueObservingOptionNew:提供更改前的值
- NSKeyValueObservingOptionOld:提供更改后的值
- NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
- NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(即一次修改有两次触发)
下面我们来看一段代码
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property (nonatomic, strong) Person *richPerson;
@end
@implementation ViewController
static void *ObservePersonContext = &ObservePersonContext;
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 首先我们先实例化一个Person对象
Person *richPerson = [[Person alloc] init];
richPerson.name = @"光头强";
richPerson.age = 38;
self.richPerson = richPerson;
[self.richPerson addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior context:ObservePersonContext];
self.richPerson.age = 50;
}
以上就是一个简单的监听注册,这里大家一定要切记context的值,当监听多个对象,以及分类也使用了KVO等情况,强烈建议大家使用context来做区分。以免造成后期维护成本过高的影响。
三. 监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if (context == ObservePersonContext) {
NSLog(@"%@",keyPath);
[self p_DoSometing];
}
}
我们来了解一下监听方法中的几个参数
- keyPath: 我们监听变化的属性名字。
- object: 我们监听的对象。
- change: 一个存储我们需求的字典类型容器,根据注册时我们选择Option来传递给我们想要的数据,有五个常量作为它的键值:
* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
一般情况下返回的都是1也就是第一个NSKeyValueChangeSetting,但是如果你监听的属性是一个集合对象的话,当这个集合中的元素被插入,
删除,替换时,就会分别返回NSKeyValueChangeInsertion,NSKeyValueChangeRemoval和NSKeyValueChangeReplacement。
* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
被监听属性改变后新值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,
该值返回的是一个数组,包含插入,替换后的新值(删除操作不会返回新值)。
* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
被监听属性改变前旧值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,
该值返回的是一个数组,包含删除,替换前的旧值(插入操作不会返回旧值)
* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
如果NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,
这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index。
* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey
如果注册监听者是options中指明了NSKeyValueObservingOptionPrior,change字典中就会带有这个key,值为NSNumber类型的YES.
- context: 同步骤二注册!
四. 注销监听
- (void)dealloc
{
[self removeObserver:self.richPerson forKeyPath:@"age"];
}
所谓注销就和通知一样,在我们编写逻辑的View或Controller销毁的时候,注销掉我们的监听。
当你向一个不是监听者的对象发送remove消息的时候(也可能是,你发送remove消息时,接受消息的对象已经被remove了一次,或者在注册为监听者前就调用了remove),xcode会抛出一个NSRangeException异常,所以,如果为了保险起见,可以把remove操作放在try/catch中。
注意:
一个监听者在其被销毁时,并不会自己注销监听,而给一个已经销毁的监听者发送通知,会造成野指针错误。所以至少保证,在监听者被释放前,将其监听注销。保证有一个add方法,就有一个remove方法。
OK,如果你认真看完了以上内容,恭喜你,你已经完全知道该如何来使用KVO了!
但是这部是本篇内容的主旨。但要深刻理解,我们继续往下看!
五. KVO的实现原理
先上两张截图方便大家参考
细心的你早已发现NSObject里的isa
指针改变了
根据两张图我们可以看出,指向了一个看着眼熟似曾相识的类(NSKVONotifying_Person
)。
没错!这个就是系统帮我们创建的Person的子类
愿意一探究竟的小伙伴可以自己在以上例子的基础上,创建一个名字为NSKVONotifying_Person
的类,编译不会有任何问题,因为OC
是运行时语言,在运行程序到注册监听的时候,系统会通过runtime的方法创建一个名字为NSKVONotifying_Person
的类,但是这时程序会导致崩溃。
因为我已经手动创建了这个类。
当然感兴趣的小伙伴也可以使用runtime
的方法在注册监听之前,动态创建NSKVONotifying_Person
类,也会产生同样的问题!
总结
KVO 很强大,没错。知道它内部实现,或许能帮助更好地使用它,或在它出错时更方便调试。但官方实现的 KVO 提供的 API 实在不怎么样。
比如,你只能通过重写 -observeValueForKeyPath:ofObject:change:context:
方法来获得通知。想要提供自定义的 selector
,不行;想要传一个 block
,门都没有。而且你还要处理父类的情况 - 父类同样监听同一个对象的同一个属性。但有时候,你不知道父类是不是对这个消息有兴趣。虽然 context
这个参数就是干这个的,也可以解决这个问题 - 在 -addObserver:forKeyPath:options:context:
传进去一个父类不知道的 context
。但总觉得框在这个 API 的设计下,代码写的很别扭。至少至少,也应该支持 block
吧。
所以在实际开发中 KVO 使用的情景并不多,更多时候还是用 Delegate 或 NotificationCenter。
后续作者会在进阶篇讲解自定义KVO的实现,敬请关注!