一、介绍
KVO(NSKeyValueObserving):是一种非正式协议,当被观察的对象(比如A)的属性(比如name)改变时,观察者(比如VC)就会得到通知,然后做出相应处理。
NSObject提供了NSKeyValueObserving协议的实现,所以几乎所有的类都可以使用KVO。
KVO 的实现依赖于 Objective-C 强大的 Runtime, Apple 的文档对 KVO 机制的实现说的很简单:KVO是用isa-swizzling技术实现的,当观察者注册了对象的属性时,被观察对象的isa指针被修改,指向中间类而不是真实类...
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
派生类在被重写的 setter 方法实现真正的通知机制(手动实现键值观察)。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
二、api
@interface NSObject(NSKeyValueObserving)
// 观察属性的通知方法
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;
@end
@interface NSObject(NSKeyValueObserverRegistration)
// 注册、移除 观察者
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
@interface NSArray(NSKeyValueObserverRegistration)
// 注册、移除 观察者 NSArray
- (void)addObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath;
// 注册、移除 观察者 NSArray
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
@interface NSOrderedSet(NSKeyValueObserverRegistration)
// 注册、移除 观察者 NSOrderedSet
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
@interface NSSet(NSKeyValueObserverRegistration)
// 注册、移除 观察者 NSSet
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
@interface NSObject(NSKeyValueObserverNotification)
// 手动触发KVO时重要方法
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
- (void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
@end
@interface NSObject(NSKeyValueObservingCustomization)
// KVO 依赖键
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// 返回yes:自动触发KVO;若要手动触发KVO,返回no
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
@end
三、例子
1、基本用法
Person类里有个name属性
- (void)dealloc{
[self.p removeObserver:self forKeyPath:@"name" context:nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.p = [Person new];
[self.p addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
self.p.name = @"a";
self.p.name = @"b";
self.p.name = @"c";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"%@ %@ %@",keyPath,object,change);
}
NSKeyValueObservingOptions:有四个值,分别是
NSKeyValueObservingOptionNew = 0x01, // 新值
NSKeyValueObservingOptionOld = 0x02, // 旧值
NSKeyValueObservingOptionInitial = 0x04,// 注册通知也会触发
NSKeyValueObservingOptionPrior = 0x08 // 值修改前后触发
2、手动KVO
Person.m
@synthesize name = _name;
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
- (NSString *)name{
return _name;
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
手动触发的话要重写automaticallyNotifiesObserversForKey方法,return NO,不然观察者的通知方法走两遍
3、KVO 依赖键
有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记。
KVO 依赖键有两种方法:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key;
+ (NSSet *)keyPathsForValuesAffecting;
下面分别用这两个方法实现:
// Student有两个属性:age和school
@interface Student : NSObject
@property (nonatomic,assign) NSInteger age;
@property (nonatomic,copy) NSString *school;
@end
// Person类
@interface Person : NSObject
@property (nonatomic,copy) NSString *info;
@property (nonatomic,strong) Student *stu;
@end
@implementation Person
- (instancetype)init
{
self = [super init];
if (self) {
_stu = [Student new];
}
return self;
}
- (NSString *)info{
return [NSString stringWithFormat:@"小敏%ld岁了,在%@上学",_stu.age,_stu.school];
}
+ (NSSet *)keyPathsForValuesAffectingInfo{
NSSet *keyPaths = [NSSet setWithObjects:@"stu.age",@"stu.school", nil];
return keyPaths;
}
//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
//
// if ([key isEqualToString:@"info"]) {
// return [NSSet setWithObjects:@"stu.age",@"stu.school", nil];
// }
//
// return [super keyPathsForValuesAffectingValueForKey:key];
//}
@end
// 调用
@interface ViewController ()
@property (nonatomic,strong) Person *p;
@end
@implementation ViewController
- (void)dealloc{
[self.p removeObserver:self forKeyPath:@"info" context:nil];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.p = [Person new];
[self.p addObserver:self forKeyPath:@"info" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"cc"];
self.p.stu.age = 18;
self.p.stu.school = @"北大";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
// descriptionWithLocale 转码处理
NSLog(@"%@ %@ %@",keyPath,object,[change descriptionWithLocale:nil]);
}
结果:
info {
kind = 1;
new = "小敏18岁了,在(null)上学";
old = "小敏0岁了,在(null)上学";
}
2018-04-13 14:19:49.729454+0800 test[5259:597332] info {
kind = 1;
new = "小敏18岁了,在北大上学";
old = "小敏18岁了,在(null)上学";
}