KVO机制学习
什么是KVO?
KVO是Key-Value-Observing的缩写,通过KVO机制对象可以得到其他对象的某个属性的变更通知。这种机制在MVC模式下显得更为重要,KVO可以让视图对象经过控制器观察模型对象的变更从而做出更新等操作。KVO不仅是Objective-C对观察者模式(Observer Pattern)的实现,也是Cocoa Binding的基础。
KVO怎么用?
KVO这一机制是基于NSKeyValueObserving协议的,Cocoa通过这个协议为所有遵循协议的对象提供了自动观察属性变化的能力。在NSObject中已经为我们实现了这一协议,所以我们不必去实现这个协议。
使用步骤:
1.注册观察者,实施监听;
//observer:观察者
//keyPath: 被观察的属性名称
//options: 观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
//第四个参数context: 上下文,可以为kvo的回调方法传值(例如设定为一个放置数据的字典)
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2.观察者实现回调方法,在回调方法中处理属性发生的变化;
//keyPath:属性名称
//object:被观察的对象
//change:变化前后的值都存储在change字典中
//context:注册观察者时,context传过来的值
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context;
3.移除观察者:
//observer:观察者
//keyPath:属性名称
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
实例:
//被观察者
@interface TestClass : NSObject
@property (nonatomic, assign)int x;
@property (nonatomic, assign)int y;
@end
@implementation TestClass
@end
//观察者
@interface ObserverClass: NSObject
@property (nonatomic, strong) TestClass *obj;
- (instancetype)initWith:(TestClass *)obj;
@end
@implementation ObserverClass
- (instancetype)initWith:(TestClass *)obj
{
if(self = [super init])
{
//不能写_obj = obj; 外部的obj发生变化,内部的_obj也会同步变化,因为_obj与obj都指向同一地址
_obj = [[TestClass alloc]init];
_obj.x = obj.x;
_obj.y = obj.y;
}
return self;
}
//观察者类需要实现的回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if([keyPath isEqualToString:@"x"] && object)
{
_obj.x = ((TestClass *)object).x;
}
else if([keyPath isEqualToString:@"y"] && object)
{
_obj.y = ((TestClass *)object).y;
}
}
@end
//在main方法中定义观察者对象和被观察者对象
int main(int argc, char * argv[]) {
@autoreleasepool {
TestClass *obj = [[TestClass alloc]init];
obj.x = 1;
obj.y = 2;
ObserverClass *observer = [[ObserverClass alloc]initWith:obj];
NSLog(@"before adding Observer");
NSLog(@"Observer x:%d", observer.obj.x);
NSLog(@"Observer y:%d", observer.obj.y);
//添加观察者
[obj addObserver:observer forKeyPath:@"x" options:NSKeyValueObservingOptionNew context:nil];
[obj addObserver:observer forKeyPath:@"y" options:NSKeyValueObservingOptionNew context:nil];
obj.x = 3;
obj.y = 4;
NSLog(@"after adding Observer");
NSLog(@"Observer x:%d", observer.obj.x);
NSLog(@"Observer y:%d", observer.obj.y);
//移除观察者
[obj removeObserver:observer forKeyPath:@"x"];
[obj removeObserver:observer forKeyPath:@"y"];
return 0;
}
}
为什么使用KVO?
我们创建一两个setter方法感觉没什么,但是如果要观察的属性非常多,那么还能一一重写setter方法来实现吗?想必大家心里已有了答案,但是利用KVO则能很好的解决上述问题。
我们自定义的类是很容易改写setter方法的,但是如果你是用一个已经编译好了的类库时要监控其中一个属性时怎么办?难道还要去重写setter方法?如果使用KVO则很轻松解决问题。
使用KVO能够方便的记录变化前的值和变化后的值,不使用KVO你还要自己来解决这些问题。
KVO让你的代码看起来更加简洁清晰易于维护。
KVO的特点
观察者观察的是属性,只有遵循 KVO 变更属性值的方式才会执行KVO的回调方法,例如是否执行了setter方法、或者是否使用了KVC赋值。
如果赋值没有通过setter方法或者KVC,而是直接修改属性对应的成员变量,例如:仅调用_name = @"newName"
,这时是不会触发kvo机制,更加不会调用回调方法的。
所以使用KVO机制的前提是遵循 KVO 的属性设置方式来变更属性值。
KVO的实现原理
KVO 的实现也依赖于 Objective-C 强大的 Runtime,Apple 使用了 isa 混写(isa-swizzling)来实现 KVO。swizzling是不是很熟悉,在Method Swizzling中我们修改了Method的IMP指向,isa-Swizzling就是修改了isa指针的指向。
首次观察某个object时,runtime会创建一个新的继承原先class的subclass。在这个新的class中,它重写了所有被观察的key,然后将object的isa指针指向新创建的class(这个指针告诉Objective-C运行时某个object到底是哪种类型的object)。所以object神奇地变成了新的子类的实例。
这些被重写的方法实现了如何通知观察者们。当改变一个key时,会触发setKey方法,但这个方法被重写了,并且在内部添加了发送通知机制。(当然也可以不走setXXX方法,比如直接修改iVar,但不推荐这么做)。
KVO的键值观察通知依赖于 NSObject 的两个方法:
-
willChangeValueForKey:
在被观察属性发生改变之前调用,通知系统该 keyPath 的属性值即将变更 -
didChangevlueForKey:
属性改变发生后调用,通知系统该 keyPath 的属性值已经变更;
之后观察者实现的observeValueForKey:ofObject:change:context:
也会被调用。并且重写观察属性的setter方法这种继承方式的注入是在运行时而不是编译时实现的。
有意思的是:苹果不希望这个机制暴露在外部。除了setters,这个动态生成的子类同时也重写了-class方法,依旧返回原先的class!如果不仔细看的话,被KVO过的object看起来和原先的object没什么两样。
以下是在网上找到的一张图,比较形象地描述了KVO的实现原理: