KVO
KVO 是 Key-Value-Observing 的简称。
KVO 是一个观察者模式。观察一个对象的属性,注册一个指定的路径,若这个对象的的属性被修改,则 KVO 会自动通知观察者。
更通俗的话来说就是任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知。
KVO 基本使用
1.// 注册观察者,实施监听;
[self.person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew
context:nil];
2.// 回调方法,在这里处理属性发生的变化;
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
3.// 移除观察者;
[self removeObserver:self forKeyPath:@“age"];
KVO 在 Apple 中的 API 文档如下:
Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …
Apple 使用了 isa 搅拌技术(isa-swizzling)来实现的 KVO 。当一个观察者注册对象的一个属性 isa 观察对象的指针被修改,指着一个中间类而不是在真正的类。
isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 runtime 系统这个对象的类是什么。
注:如果对 runtime 不很清楚的话可以看下这篇文章Objective-C 中的 Runtime
举个栗子:
_person = [[Person alloc] init];
/**
* 添加观察者
*
* @param observer 观察者
* @param keyPath 被观察的属性名称
* @param options 观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
* @param context 上下文,可以为nil。
*/
[_person addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
/**
* KVO回调方法
*
* @param keyPath 被修改的属性
* @param object 被修改的属性所属对象
* @param change 属性改变情况(新旧值)
* @param context context传过来的值
*/
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
{
NSLog(@"%@对象的%@属性改变了:%@",object,keyPath,change);
}
/**
* 移除观察者
*/
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"age"];
}
KVO 实现原理
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。 派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
派生类 NSKVONotifying_Person 剖析:
在这个过程,被观察对象的 isa 指针从指向原来的 Person 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_Person 类,来实现当前类属性值改变的监听。
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为 NSKVONotifying_Person 的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_Person 的中间类,并指向这个中间类了。
因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。
子类 setter 方法剖析:
KVO 在调用存取方法之前总是调用 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在调用存取方法之后总调用
}
总结: KVO 的本质就是监听对象的属性进行赋值的时候有没有调用 setter 方法
- 系统会动态创建一个继承于 Person 的 NSKVONotifying_Person
- person 的 isa 指针指向的类 Person 变成 NSKVONotifying_Person,所以接下来的 person.age = newAge 的时候,他调用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子类)的 setter 方法
- 重写
NSKVONotifying_Person的setter方法:[super setName:newName]
- 通知观察者告诉属性改变。
KVO 应用
监听 ScrollView 的 contentOffSet 属性,采取相应的措施:
[scrollview addObserver:self
forKeyPath:@“contentOffset
options:NSKeyValueObservingOptionNew
context:nil];
下面是用 KVO 写的一个通过监听 scrollview 的 contentOffSet 实现的一个小刷新功能,感兴趣的可以看下。
KVO 总结
KVO 是一个对象能观察另一个对象属性的值,KVO 适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。KVO 只能对属性做出反应,不会用来对方法或者动作做出反应。
优点:
- 提供一个简单的方法来实现两个对象的同步。
- 能够提供观察的属性的新值和旧值。
- 每一次属性值改变都是自动发送通知,不需要开发者手动实现。
- 用 keypath 来观察属性,因此也可以观察嵌套对象。
缺点:
- 观察的属性必须使用字符串来定义,因此编译器不会出现警告和检查
- 只能重写回调方法来后去通知,不能自定义 selector。当观察多个对象的属性时就要写"if"语句,来判断当前的回调属于哪个对象的属性的回调。