Introduction to Key-Value Observing Programming Guide
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
Important: In order to understand key-value observing, you must first understand key-value coding.
一、KVO简介
KVO 全称 Key-Value Observing。中文叫键值观察。KVO其实是一种观察者模式,观察者在键值改变时会得到通知,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身。相比Notification,KVO更加的简单直接。
KVO的操作方法由NSKeyValueCoding提供,而他是NSObject的类别,也就是说ObjC中几乎所有的对象都支持KVO操作。
二、运用键值观察
KVO的使用就是简单的三步曲:
1、注册需要观察的对象的属性
如果我们已经有了包含可供键值观察属性的类,那么就可以通过在该类的对象(被观察者)上调用名为NSKeyValueObserverRegistration 的category方法将观察者对象与被观察者对象注册
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
2、 处理变更通知
观察者需要实现名为 NSKeyValueObserving 的 category 方法来处理收到的变更通知:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
实现observeValueForKeyPath:ofObject:change:context:方法,这个方法当观察的属性变化时会自动调用.在这个方法中还通过NSKeyValueObservingOptionNew这个参数要求把新值在dictionary中传递过来。
3、取消注册需要观察的对象
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
示例程序:
main.m
// key - valued - observer 键值观察
//1、注册被观察者
//2、观察者接收被观察者值的变化
//3、不使用时一定记得remove掉
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"
#import "Watcher.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person * p = [[Person alloc] init];
Watcher * w = [[Watcher alloc] init];
p.watcher = w;
p.age = 20;
NSLog(@"%@",p);
[p registerObserver];
p.age = 12;
NSLog(@"%@",p);
p.age = 30;
}
return 0;
}
Person.h
#import <Foundation/Foundation.h>
#import "Watcher.h"
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, strong) Watcher * watcher;
- (void)registerObserver;
@end
Person.m
#import "Person.h"
@implementation Person
- (NSString *)description {
return [NSString stringWithFormat:@"%s", object_getClassName(self)];
}
- (void)registerObserver {
[self addObserver:self.watcher forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
- (void)dealloc {
[self removeObserver:self.watcher forKeyPath:@"age"];
}
@end
Watcher.h
#import <Foundation/Foundation.h>
@interface Watcher : NSObject
@end
Watcher.m
#import "Watcher.h"
@implementation Watcher
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {
NSLog(@"%@",change[@"new"]);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
@end
运行结果:
思考,当被观察者属性重写设置,但值并没有改变,每次设置都会触发KVO,如果有多次设置重复设置相同的数值呢?那么需要手动设置KVO。
三、手动实现键值观察KVO
示例程序:
main.m
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Person.h"
#import "Watcher.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person * p = [[Person alloc] init];
Watcher * w = [[Watcher alloc] init];
p.watcher = w;
p.age = 20;
NSLog(@"%@",p);
[p registerObserver];
p.age = 12;
NSLog(@"%@",p);
p.age = 12;
}
return 0;
}
Person.h
#import <Foundation/Foundation.h>
#import "Watcher.h"
@interface Person : NSObject
//KVO 需要实现setter/getter方法
@property (nonatomic, assign) int age;
@property (nonatomic, strong) Watcher * watcher;
- (void)registerObserver;
@end
Person.m
#import "Person.h"
@implementation Person
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = YES;
if ([key isEqualToString:@"age"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)setAge:(int)age {
//手动设置KVO
if (_age != age) {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
}
- (NSString *)description {
return [NSString stringWithFormat:@"%s", object_getClassName(self)];
}
@end
Watcher.h
#import <Foundation/Foundation.h>
@interface Watcher : NSObject
@end
Watcher.m
#import "Watcher.h"
@implementation Watcher
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"age"]) {
NSLog(@"%@",change[@"new"]);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
@end
执行结果:
首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;
其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
四、键值观察实现原理:
键值观察用处很多,Core Binding 背后的实现就有它的身影,那键值观察背后的实现又如何呢?想一想在上面的自动实现方式中,我们并不需要在被观察对象 Target 中添加额外的代码,就能获得键值观察的功能,这很好很强大,这是怎么做到的呢?答案就是 Objective C 强大的 runtime 动态能力,下面我们一起来窥探下其内部实现过程。
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
简单的说主要是因为KVO的实现使用了isa-swizzling。在程序运行时Person会生成一个派生类NSKVONotifying_Person,在这个派生类中重写基类中任何被观察属性的setter方法,用来欺骗系统顶替原先的类。在setter方法中实现真正的通知机制.
苹果官方给出的原理简介:
苹果官方KVO文档: