iOS中的KVO简介
1.概述
很多童鞋在iOS开发中都听过所谓的KVO,其实这只是一个缩写,也只是一种开发模式,它的全称是Key-Value Observing (观察者模式) 是苹果Fundation框架下提供的一种开发机制,使用KVO,可以方便地对指定对象的某个属性进行观察,当属性发生变化时,进行通知,告诉开发者属性旧值和新值对应的内容这种开发模式在实际开发中应用场景还是很多的,同时也是很多大企业在面试过程中必不可少的面试题之一,也是考察中高级程序员的核心标准,所以,搞清楚KVO的原理和使用方法也是所有iOS程序员的必备的技能和核心基础。
2.原理
要了解KVO的机制我们必须对KVO底层实现要有一定的掌握。苹果 使用了 isa 混写(isa-swizzling)来实现 KVO 。也就是说当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
(备注: isa 混写(isa-swizzling)isa:is a kind of ; swizzling:混合,搅合;)
说起isa指针,我们可以用下面的两个图来解释:
isa指针上图我们可以看到,一个实例变量的isa指针是指向它的类对象的,然而一个类对象的isa指针是指向其元类对象。一层一层的递进。而KVO在被创建的时候则会自动产生一个NSKVONotifying_A 的新类,而实例对象的isa则会指向这个新类,新类的isa指针则继续指向这个实例对象的类对象,从而监听这个类对象属性的set方法,用两张图来打个比方,假如我们新建一个Person对象,如果没有使用KVO应该是这样的:
普通创建类 使用KVO之后这里我使用了两张MJ老师制作的PPT来引用我上述的说法,MJ老师做的图相当的清楚和直观,我们可以看到,在普通没有使用KVO时,我们创建一个Person类之后Person的实例对象的isa指针是直接指向其类对象的,然后由类对象来处理属性和属性对应的setter方法,但是第二个图我们就可以看到,如果使用了KVO之后,那系统就会通过Runtime机制动态创建一个NSKVONotifying_A的对象,而Person的isa指针就会指向这个类对象,由这个对象去监听Person的类对象。
NSKVONotifying_A子类setter方法剖析
KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用 2 个方法:被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
-(void)setName:(NSString *)newName{
[self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用
}
ps:注意点:
观察者观察的是属性,只有遵循 KVO 变更属性值的方式才会执行 KVO 的回调方法,例如是否执行了 setter 方法、或者是否使用了 KVC 赋值。
如果赋值没有通过 setter 方法或者 KVC,而是直接修改属性对应的成员变量,例如:仅调用 _name = @"newName",这时是不会触发 KVO 机制,更加不会调用回调方法的。
所以使用 KVO 机制的前提是遵循 KVO 的属性设置方式来变更属性值。
3.如何使用
那么在接触完KVO的原理和概念之后我们就要了解如何去使用KVO了,其实KVO的使用并不难,我们这就来上代码:
1. 注册Observer:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
即:
addObserver:forKeyPath:options:context:
上述的参数含义:
observer:观察者,需要响应属性变化的对象。该对象必须实现 observeValueForKeyPath:ofObject:change:context: 方法。
keyPath:要观察的属性名称。要和属性声明的名称一致。
options:对KVO机制进行配置,修改KVO通知的时机以及通知的内容。
context:context是一个c指针,可以传入任意类型的对象,在观察者接收通知回调的方法 observeValueForKeyPath:ofObject:change:context: 中可以接收到这个对象,是KVO中的一种传值方式。这个参数可以用来区分同一对象对同一个属性的多个不同的监听。
这里有两点是需要注意的:
-
observeValueForKeyPath:ofObject:change:context:方法的对象是目标对象,observer是观察者对象,keyPath是目标对象的属性。
-
注意,在注册了Observer后,一定要在合适时机移除注册,否则会crash。移除注册的两种方法:
- (void)removeObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
context:(void *)context
苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。
第二种方法带有context属性,主要是用来区分不同的观察者Observer的。
如果observer没有监听keyPath属性,则调用这两个方法会抛出异常并崩溃。所以,必须确保先注册了观察者,才能调用移除方法。。实际上,在添加观察者的时候,观察者对象与被观察属性所属的对象都不会被retain,然而在这些对象被释放后,相关的监听信息却还存在,KVO做的处理是直接让程序崩溃。
- options参数是一个枚举类型,共有四种取值方式:
enum {
NSKeyValueObservingOptionNew = 0x01, //新值
NSKeyValueObservingOptionOld = 0x02, //旧值
NSKeyValueObservingOptionInitial = 0x04, //
NSKeyValueObservingOptionPrior = 0x08
};
- NSKeyValueObservingOptionNew:接收方法中使用change参数传入变化后的新值,键为:NSKeyValueChangeNewKey;
- NSKeyValueObservingOptionOld:接收方法中使用change参数传入变化前的旧值,键为:NSKeyValueChangeOldKey;
- NSKeyValueObservingOptionInitial:注册之后立即调用一次接收方法。如果还如果配置了NSKeyValueObservingOptionNew,change参数内容会包含新值,键为:NSKeyValueChangeNewKey。
- NSKeyValueObservingOptionPrior:如果加入这个参数,接收方法会在变化前后分别调用一次,共两次,变化前的通知change参数包含notificationIsPrior = 1。其他内容根据NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld的配置确定。
-
注意:options参数可以配置多个,如:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld,使用 | 或运算符连接。
-
调用addObserver:forKeyPath:options:context:方法时,观察者对象与被观察属性所属的对象都不会被retain,也就是说,引用计数不会加1。
-
可以重复添加监听:可以多次调用addObserver:..方法,将同一对象注册为同一属性的的观察者(参数可以完全相同,可以使用context参数进行区分)。这些观察者会并存。
2.接收通知
当被监听的属性的值发生变化时,KVO会自动通知注册了的观察者。上文提到,观察者必须实现以下方法,这个方法就是观察者接收通知的方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
参数:
object:目标对象,即所监听的对象,也就是所监听的属性所属的对象。
change:是传入的变化量,通过在注册时用options参数进行的配置,会包含不同的内容。
- change参数
除了根据options参数控制的change参数内容,默认change参数会包含一个NSKeyValueChangeKindKey键值对,传递被监听属性的变化类型:
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
- NSKeyValueChangeSetting:属性的值被重新设置;
- NSKeyValueChangeInsertion、NSKeyValueChangeRemoval NSKeyValueChangeReplacement:表示更改的是集合属性,分别代表插入、删除、替换操作。
- 如果NSKeyValueChangeKindKey参数是针对集合属性的三个之一,change参数还会包含一个NSKeyValueChangeIndexesKey键值对,表示变化的index。
- change字典里,新值的key为“new”,旧值的key为“old”,变化类型的key为“kind”。
3. 示例
-(void)setKVO{
_p = [[Person alloc]init];
[_p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
//点击改变对象的名字
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_p.name = @"leon";
}
//接收改变的新旧值
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
// NSKeyValueChangeNewKey == @"new"
NSString *new = change[NSKeyValueChangeNewKey];
// NSKeyValueChangeOldKey == @"old"
NSString *old = change[NSKeyValueChangeOldKey];
NSLog(@"%@-%@",new,old);
}
本文参考链接:
iOS 关于KVO的一些总结
iOS开发 -- KVO的实现原理与具体应用