KVO的定义
键值观察,对象采用的一种非正式协议,用于将其他对象的指定属性的更改通知给对象。可以观察到任何对象属性,包括简单属性,一对一关系和一对多关系。一对多关系的观察者被告知所做更改的类型,以及更改涉及哪些对象。NSObject
提供了NSKeyValueObserving协议的实现,该协议为所有对象提供了自动观察功能。开发者可以通过禁用自动观察者通知,并使用此协议中的方法实施手动通知来进一步优化通知。
常用方法和参数
注册观察
注册观察者
-addObserver:forKeyPath:options:context:
移除观察者。
-removeObserver:forKeyPath:
在给定上下文的情况下,移除观察者。
-removeObserver:forKeyPath:context:
变更通知
键值发生改变时,通知观察对象。
-observeValueForKeyPath:ofObject:change:context:
通知观察者变更
通知观察到的对象给定属性的值即将更改。
-willChangeValueForKey:
通知观察到的对象给定属性的值已更改。
-didChangeValueForKey:
通知观察对象对于指定的有序多对关系,将在给定的索引处执行指定的更改。
-willChange:valuesAtIndexes:forKey:
通知观察对象指定的多对多关系在索引上发生了指定的更改。
-didChange:valuesAtIndexes:forKey:
通知观察对象即将对指定的无序多对关系进行指定的更改。
-willChangeValueForKey:withSetMutation:usingObjects:
通知观察对象对指定的无序对多关系进行了指定的更改。
-didChangeValueForKey:withSetMutation:usingObjects:
观察定制
是否允许自动键值观察。
+automaticallyNotifiesObserversForKey:
示例:name属性不允许自动观察
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return YES;
}
// 手动观察name属性
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
为属性的值返回一组键路径,观察多个键值时使用,任一键值有更新,都会被通知到。
+keyPathsForValuesAffectingValueForKey:
示例:观察Person的dog属性中name、age的变化
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) Dog *dog;
@property (nonatomic, strong) NSMutableArray *arr;
@end
@implementation Person
- (instancetype)init {
self = [super init];
if (self) {
_dog = [[Dog alloc] init];
_arr = [NSMutableArray array];
}
return self;
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
// 此处一定要带下划线
NSArray *arr = @[@"_dog.name", @"_dog.age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
}
return keyPaths;
}
@end
@interface Dog : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
#import "KVOViewController.h"
#import "Person.h"
@interface KVOViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation KVOViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
Person *person = [Person new];
_person = person;
// 观察person内dog属性的name和age变化
// [_person addObserver:self forKeyPath:@"dog.name" options:(NSKeyValueObservingOptionNew) context:nil];
// [_person addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
[_person addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
_person.dog.name = name;
_person.dog.age = i++;
}
@end
如果给定数组中指定的任何属性发生更改,则将观察对象配置为发布给定属性的更改通知。不推荐使用
+setKeys:triggerChangeNotificationsForDependentKey:
返回一个指针,该指针标识有关向观察对象注册的所有观察者的信息。
observationInfo
参数
可以观察到的变化类型。
NSKeyValueChange
NSKeyValueChangeSetting // 观察set方法
NSKeyValueChangeInsertion // 观察集合 insert 元素操作
NSKeyValueChangeRemoval // 观察集合 remove 元素操作
NSKeyValueChangeReplacement // 观察集合 replace 元素操作
可以在变更字典中返回的值。
NSKeyValueObservingOptions
NSKeyValueObservingOptionOld // 表示在change字典中包含了改变前的值。
NSKeyValueObservingOptionNew // 表示在change字典中包含新的值。
NSKeyValueObservingOptionInitial // 在注册观察者的方法return的时候就会发出一次通知。
NSKeyValueObservingOptionPrior // 在值发生改变前、后各发出一次通知,每次change都会有两个通知。
可以显示在更改字典中的键。
NSKeyValueChangeKey
NSKeyValueChangeKindKey // 指明了变更的类型,值为“NSKeyValueChange”枚举中的某一个,类型为NSNumber
NSKeyValueChangeNewKey // 被监听属性改变后新值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,该值返回的是一个数组,包含插入,替换后的新值(删除操作不会返回新值)
NSKeyValueChangeOldKey // 被监听属性改变前旧值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,该值返回的是一个数组,包含删除,替换前的旧值(插入操作不会返回旧值)
NSKeyValueChangeIndexesKey // 如果NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index。
NSKeyValueChangeNotificationIsPriorKey // 如果注册监听者是options中指明了NSKeyValueObservingOptionPrior,change字典中就会带有这个key,值为NSNumber类型的YES.
可以对无序集合进行的突变类型。
NSKeyValueSetMutationKind
NSKeyValueUnionSetMutation // 已将指定集中的观察者添加到观察对象。
NSKeyValueMinusSetMutation // 正在从观察对象中除去指定集合中的观察者
NSKeyValueIntersectSetMutation // 正在从观察对象中移除不在指定集合中的观察者。
NSKeyValueSetSetMutation // 一组观察者正在替换观察对象中的现有对象。
KVO的内部实现
当我们对Person对象的name属性进行观察的时候,实际上会在-addObserver:forKeyPath:options:context:
内部动态创建一个Person的子类 NSKVONotifying_Person,动态修改观察对象的类型,观察子类,然后重写子类的set方法来实现。
可以发现,在addObserver:forKeyPath:options:context:
执行前,person的isa指针指向Person,addObserver:forKeyPath:options:context
执行后,person的isa指针指向了NSKVONotifying_Person,说明在addObserver:forKeyPath:options:context
方法内部发生了指针混写。
自定义KVO的监听
新建一个NSObject的分类NSObject+JRKVO,定义方法:jr_addObserver: forKeyPath: options: context:
,在方法实现中如下代码:
#import "NSObject+JRKVO.h"
#import
static const char *JRKVO_observer = "JRKVO_observer";
static const char *JRKVO_getter = "JRKVO_getter";
static const char *JRKVO_setter = "JRKVO_setter";
@implementation NSObject (JRKVO)
- (void)jr_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context {
// 创建、注册子类
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"JRKVO_%@", oldClassName];
Class newClass = objc_getClass(newClassName.UTF8String);
if (!newClass) {
// 创建类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(newClass);
}
// setter方法首字母大写
NSString *newKeyPath = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
NSString *setName = [NSString stringWithFormat:@"set%@", newKeyPath];
SEL setSEL = NSSelectorFromString([setName stringByAppendingString:@":"]);
// 动态修改监听的类型,监听子类
object_setClass(self, newClass);
// 子类添加setter方法
Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
const char *types = method_getTypeEncoding(getMethod);
class_addMethod(newClass, setSEL, (IMP)setMethod, types);
// 保存Observer
objc_setAssociatedObject(self, JRKVO_observer, observer, OBJC_ASSOCIATION_ASSIGN);
// 保存setter、getter方法
objc_setAssociatedObject(self, JRKVO_setter, setName, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, JRKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
void setMethod(id self, SEL _cmd, id newValue) {
// 获取setter、getter方法
NSString *setName = objc_getAssociatedObject(self, JRKVO_setter);
NSString *getName = objc_getAssociatedObject(self, JRKVO_getter);
// 保存子类类型
Class class = [self class];
// isa指向原类
object_setClass(self, class_getSuperclass(class));
// 调用原类getter方法,获取旧值
id oldValue = objc_msgSend(self, NSSelectorFromString(getName));
// 调用原类setter方法
objc_msgSend(self, NSSelectorFromString([setName stringByAppendingString:@":"]), newValue);
id observer = objc_getAssociatedObject(self, JRKVO_observer);
NSMutableDictionary *change = [@{} mutableCopy];
if (newValue) {
change[NSKeyValueChangeNewKey] = newValue;
}
if (oldValue) {
change[NSKeyValueChangeOldKey] = oldValue;
}
// 通知观察者
objc_msgSend(observer, @selector(observeValueForKeyPath: ofObject: change: context:), getName, self, change, nil);
// isa重新指向子类
object_setClass(self, class);
}
@end
调用jr_addObserver: forKeyPath: options: context:
方法,观察person的name属性
[_person jr_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
改变person的name属性的值
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
// _person.name = name;
objc_msgSend(_person, @selector(setName:), name);
}
set方法已经执行,并且观察者回调方法中已经输出name新值、旧值。
自定义KVO的监听 带block回调
在上面创建的NSObject分类NSObject+JRKVO中,定义方法:jr_addObserverForKeyPath: options: callbak:
,在方法实现中如下代码:
#import "NSObject+JRKVO.h"
#import
static const char *JRKVO_getter = "JRKVO_getter";
static const char *JRKVO_setter = "JRKVO_setter";
static const char *JRKVO_callback = "JRKVO_callback";
@implementation NSObject (JRKVO)
- (void)jr_addObserverForKeyPath:(NSString *_Nonnull)keyPath
options:(NSKeyValueObservingOptions)options
callbak:(JRKVOCallbackBlock _Nonnull )block {
// 创建、注册子类
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"JRKVO_%@", oldClassName];
Class newClass = objc_getClass(newClassName.UTF8String);
if (!newClass) {
// 创建类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(newClass);
}
// setter方法首字母大写
NSString *newKeyPath = [[[keyPath substringToIndex:1] uppercaseString] stringByAppendingString:[keyPath substringFromIndex:1]];
NSString *setName = [NSString stringWithFormat:@"set%@", newKeyPath];
SEL setSEL = NSSelectorFromString([setName stringByAppendingString:@":"]);
// 动态修改监听的类型,监听子类
object_setClass(self, newClass);
// 子类添加setter方法
Method getMethod = class_getInstanceMethod([self class], @selector(keyPath));
const char *types = method_getTypeEncoding(getMethod);
class_addMethod(newClass, setSEL, (IMP)setMethod, types);
// 保存block
objc_setAssociatedObject(self, JRKVO_callback, block, OBJC_ASSOCIATION_COPY);
// 保存setter、getter方法
objc_setAssociatedObject(self, JRKVO_setter, setName, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, JRKVO_getter, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
void setMethod(id self, SEL _cmd, id newValue) {
// 获取setter、getter方法
NSString *setName = objc_getAssociatedObject(self, JRKVO_setter);
NSString *getName = objc_getAssociatedObject(self, JRKVO_getter);
// 保存子类类型
Class class = [self class];
// isa指向原类
object_setClass(self, class_getSuperclass(class));
// 调用原类getter方法,获取旧值
id oldValue = objc_msgSend(self, NSSelectorFromString(getName));
// 调用原类setter方法
objc_msgSend(self, NSSelectorFromString([setName stringByAppendingString:@":"]), newValue);
NSMutableDictionary *change = [@{} mutableCopy];
if (newValue) {
change[NSKeyValueChangeNewKey] = newValue;
}
if (oldValue) {
change[NSKeyValueChangeOldKey] = oldValue;
}
// 回调setter方法值的改变
JRKVOCallbackBlock callback = objc_getAssociatedObject(self, JRKVO_callback);
if (callback) {
callback(change);
}
// isa重新指向子类
object_setClass(self, class);
}
@end
调用jr_addObserverForKeyPath: options: callbak:
方法,观察person的name属性变化。
观察多个键值的变化
通keyPathsForValuesAffectingValueForKey:
实现多个键值观察
示例:观察Person的dog属性中name、age的变化
Person 类中实现如下方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
// 此处一定要带下划线
NSArray *arr = @[@"_dog.name", @"_dog.age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
}
return keyPaths;
}
VC中观察Person的dog属性
[_person addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"dog 改变了%@", change);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
_person.dog.name = name;
_person.dog.age = i++;
}
点击屏幕,改变dog对象name、age的值,打印结果
通过keyPathsForValuesAffectingValueForKey:
把多个keyPath放在一个集合set中,统一观察,dog的任何一个属性发生变化,都会通知到。比起单个keyPath观察,确实方便了很多。
数组的观察
通过KVC中的mutableArrayValueForKey:
实现数组的观察,数组中元素改变(增、删、改)都可以观察到。
示例代码:
#import "KVOViewController.h"
#import "Person.h"
#import "NSObject+JRKVO.h"
#import
@interface KVOViewController ()
@property (nonatomic, strong) Person *person;
@end
@implementation KVOViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
Person *person = [Person new];
_person = person;
// 观察 person的arr属性的变化, 数组元素的增删改
[_person addObserver:self forKeyPath:@"arr" options:(NSKeyValueObservingOptionNew) context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"Person中的arr改变了%@", change);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
static NSInteger i = 0;
NSString *name = [NSString stringWithFormat:@"%ld", (long)i++];
// 通过KVC获取到数组,
NSMutableArray *arr = [_person mutableArrayValueForKey:@"arr"];
// add 对应 NSKeyValueChange 为 NSKeyValueChangeInsertion, 即:kind 为 2
[arr addObject:name];
// replace 对应 NSKeyValueChange 为 NSKeyValueChangeReplacement, 即:kind 为 4
[arr replaceObjectAtIndex:0 withObject:@"1111"];
// remove 对应 NSKeyValueChange 为 NSKeyValueChangeRemoval, 即:kind 为 3
[arr removeObjectAtIndex:0];
}
@end
点击屏幕,操作数组元素的增、删、改,打印结果:
执行完
mutableArrayValueForKey:
后,观察arr的类型已经变成NSKeyValueMutableArray类型,其实mutableArrayValueForKey:
内部动态创建了NSMutableArray的子类NSKeyValueMutableArray,然后指针混写指向子类,并返回子类,后续观察arr实际上是观察子类。
这里的kind = 2,kind = 3,kind = 4, 分别对应NSKeyValueChange
的 NSKeyValueChangeInsertion
、NSKeyValueChangeRemoval
、NSKeyValueChangeReplacement
。也就是说KVO不仅仅能观察对象的set方法,当对象为集合类型时,也能观察集合元素的增、删、改,只不过观察集合对象要通过KVC的mutableArrayValueForKey:
方法来获取集合。类似通过person.arr
的方式获取数组,操作数组元素的增、删、改是观察不到的。