KVO
今天写一下KVO的实现原理。
一、应用
1、API
(1)给对象添加KVO监听
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
observer:观察对象,需要实现
observeValueForKeyPath: ofObject: change: context:
。
keyPath:被观察的属性。
options:发送通知的时机(属性值改变前or改变后通知)
context:(目前未用到过该参数)上下文信息,通常为nil
。
- NSKeyValueObservingOptions:
NSKeyValueObservingOptionNew 返回新值
NSKeyValueObservingOptionOld 返回旧值
NSKeyValueObservingOptionInitial 注册的时候发一次通知,改变后也发送一次通知
NSKeyValueObservingOptionPrior 改变之前发一次,改变之后再发一次
(2)接收通知
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context;
keyPath:被观察的属性
object:被观察对象
context:添加监听时传过来的上下文信息
change:字典,keys有以下五种:NSKeyValueChangeNewKey 新值
NSKeyValueChangeOldKey 旧值
NSKeyValueChangeIndexesKey 观察容器属性时会返回的索引值
NSKeyValueChangeNotificationIsPriorKey
NSKeyValueChangeKindKey 四种修改类型,如下:NSKeyValueChangeSetting = 1 赋值 SET
NSKeyValueChangeInsertion = 2 插入 insert
NSKeyValueChangeRemoval = 3 移除 remove
NSKeyValueChangeReplacement = 4 替换 replace
(3)移除通知
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
observer:观察对象
keyPath:观察属性
被观察对象在销毁前必须要移除监听。且被重复添加N次的被观察对象,也需要销毁N次
2、代码示例
先定义两个类KYDog
和KYUser
,且在KYUser
中有KYDog
类型的属性。控制器中添加KYUser
属性。
- 公用代码:
@interface KYDog : NSObject
/** 狗的名字 */
@property (nonatomic, strong) NSString *name;
/** 狗的年龄 */
@property (nonatomic, assign) int age;
@end
@interface KYUser : NSObject
/** ID */
@property (nonatomic, strong) NSString *userId;
/** 狗�� */
@property (nonatomic, strong) KYDog *dog;
/** 数组 */
@property (nonatomic, strong) NSMutableArray *arr;
@end
@interface ViewController ()
@property (nonatomic, strong) KYUser *user;
@end
(1)观察普通类型属性
观察user中的userId属性:
- (void)viewDidLoad {
[super viewDidLoad];
self.user = [[KYUser alloc] init];
// 1、添加KVO监听
[self.user addObserver:self forKeyPath:@"userId" options:NSKeyValueObservingOptionNew context:nil];
}
// 2、接收监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", keyPath);
NSLog(@"%@", object);
/*
NSKeyValueChangeKindKey;
NSKeyValueChangeNewKey;
NSKeyValueChangeOldKey;
NSKeyValueChangeIndexesKey;
*/
NSKeyValueChangeNotificationIsPriorKey
NSLog(@"%@", change[NSKeyValueChangeNewKey]);
NSLog(@"%@", (__bridge id)(context));
}
// 3、触发修改属性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.user.userId = @"123456789";
}
// 4、移除监听
- (void)dealloc {
[self.user removeObserver:self forKeyPath:@"userId"];
}
(2)手动触发KVO
上面的方法在userId
值发生变化时会自动触发通知。但是在某些情况下,对于有些情况的值变动并不想被观察到,该情况下可使用手动触发的KVO的方法。
在被观察对象KYUser
中重写下方类方法:
//默认自动模式YES,若返回NO,不发送通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
此时修改userId的值,通知将不被触发。在修改值的前后写上手动通知的方法:
// willChange 和 didChange 一般成对儿出现
[self.user willChangeValueForKey:@"userId"];
self.user.userId = @"123456";
[self.user didChangeValueForKey:@"userId"];
(3)观察被观察者中自定义类型中的属性
- 观察
KYUser
中KYDog
属性,可以直接点语法到需要观察的属性。例如在控制器中观察user.dog.name
:
[self.user addObserver:self forKeyPath:@"dog.name" options:NSKeyValueObservingOptionNew context:nil];
- 观察
user.dog
属性中的多个属性。例如在控制器中观察user.dog.name
、user.dog.age
等。很笨的方法就是添加多个addObserver
,但是若user.dog
中有很多很多的属性需要观察,这样的需求可以在被观察者KYUser
中重写类方法:
// 返回一个容器,里面放字符串类型,监听容器中的属性
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"dog"]) {
NSArray *arr = @[@"_dog.name", @"_dog.age"];
keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
}
return keyPaths;
}
接下来在控制器中就可以直接观察dog
,上面返回的集合中的属性就都会被观察到:
[self.user addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil];
通知中打印change:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
change
字典中new
值是KYDog
类型。
(4)观察容器类属性
观察user.arr
数组属性,使用[self.user.arr addObject:@"value_1"]
,并不能被观察到,因为观察者实现原理是对set
方法的监听。但是可以结合KVC实现对容器属性的监听:
// 添加观察数组属性
[self.user addObserver:self forKeyPath:@"arr" options:NSKeyValueObservingOptionNew context:nil];
// 通过KVC获取到属性值,这样才能观察到arr属性的修改
NSMutableArray *tempArr = [self.user mutableArrayValueForKey:@"arr"];
[tempArr addObject:@"value_1"];
观察tempArr类型:
其实在添加观察user.arr
属性时,就动态生成了NSMutableArray
的子类NSKeyValueNotifyingMutableArr
,并重写了它的addObject:
等方法,加入了willChange
和didChange
方法。
二、实现原理
KVO的实现原理就是通过运行时,替换被观察对象KYUser
的isa指针,创建其子类对象NSKVONotifying_KYUser
,重写被观察属性set方法,将被观察属性值的变法发送给指定的通知方法。
1、KVO动态创建的子类
添加观察的时候,动态创建了子类:
2、自定义KVO实现
- 根据1的提示,创建NSObject的分类category。定义并实现下面的方法:
- (void)KY_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
// 1、自定义NSKVONotifying子类
NSString *oldClassName = NSStringFromClass(self.class);
NSString *newClassName = [NSString stringWithFormat:@"KYKVO_%@", oldClassName];
// 创建KVO子类
Class KYKVO_Class = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
// 注册KVO子类
objc_registerClassPair(KYKVO_Class);
// 2、动态修改类型,指向新创建的子类(改变isa指针)
object_setClass(self, KYKVO_Class);
// 3、动态添加set方法(重写父类方法就是添加)
// v@:@ v表示customMethod返回值void,@表示第一个参数是OC对象,: 表示SEL类型,@表示第三个参数为OC对象
class_addMethod(KYKVO_Class, @selector(setUserId:), customMethod, "");
}
// 前两个隐藏参数不可省略
void customMethod(id self, SEL _cmd, NSString *newValue) {
id class = [self class];
// 让自己指向父类
object_setClass(self, class_getSuperclass([self class]));
objc_msgSend(self, @selector(setUserId:), newValue);
// 赋值完了之后isa再指向自己
object_setClass(self, class);
// 属性值发生变化的前后,可以使用通知、代理或block的方式将值的变法发送出去。后面的步骤比较简单直接略过了。。。
}
代码中使用运行时动态创建了自定义的KVO子类,并添加了setUserId:
方法。真实的逻辑肯定比这个要复杂的多,上面的代码只是很简单的模拟了一下 user.userId
被观察时的情况。然后再自己定义通知方法,将观察到的情况发送给观察者。后面的发送观察结果给观察者实现方法比较简单,直接略过了。。。。