KVO 概述
KVO,全称为 Key-Value observing,中文名为键值观察,可以理解为监听模式,对象 A 监听对象 B 的属性 age,当属性 age 发生改变的时候,对象 A 收到通知,做相应的操作
KVO 使用基本步骤
- 注册观察者
- 实现 KVO 回调
- 移除观察者
注意:添加观察者与移除观察者必须成对出现,不然会发生不可控错误
KVO 常用方法
- 基本使用
@interface Person : NSObject
@property(nonatomic,copy)NSString * age;
@end
@interface ViewController ()
@property(nonatomic,strong)Person * p;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person * p = [[Person alloc]init];
_p = p;
// 添加观察者
[_p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
}
// 监听属性变化回调
/*
* keyPath 观察者的key
* object 观察者key所属的对象
* context 值改变的上下文
* change 改变的内容,新值or旧值
*/
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context {
NSLog(@"监听到改变:%@对象的属性:%@===发生改变:change:%@",object,keyPath,change);
}
// 修改属性的值,触发方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int i = 0;
i++;
// 修改值
self.p.age = [NSString stringWithFormat:@"%d",i];
}
// 移除观察者
- (void)dealloc {
[_person removeObserver:self forKeyPath:@"age"];
}
@end
- KVO 自动触发与手动触发
KVO 观察的开启和关闭有两种方式,自动和手动
- 返回
YES表示监听 - 返回
NO不监听
// key 表示监听属性
// 默认返回YES
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if([key isEqualToString:@"age"]) {
NSLog(@"关闭了age自动触发");
return NO;
}
return YES;
}
- 自动监听关闭的时候,可以通过手动触发回调
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int i = 0;
i++;
// 1. willChangeValueForKey
// 2. set赋值
// 3. didChangeValueForKey
[self.p willChangeValueForKey:@"age"];
self.p.age = [NSString stringWithFormat:@"%d",i];
[self.p didChangeValueForKey:@"age"];
}
- 通过 KVO 观察一个
对象里面的一个对象属性的里面的属性
@interface Tool : NSObject
@property(nonatomic,copy)NSString * isLogin;
@end
@interface Person : NSObject
// Tool对象
@property(nonatomic,strong)Tool * toolObj;
@end
- (void)viewDidLoad {
[super viewDidLoad];
// 监听对象的对象的属性
// toolObj 不为nil时候,改变toolObj属性值,会触发回调,为nil则不会收到回调
[self.p addObserver:self forKeyPath:@"toolObj.isLogin" options:NSKeyValueObservingOptionNew context:nil];
// 这种监听方式,改变toolObj属性值不会触发回调
[self.p addObserver:self forKeyPath:@"toolObj" options:NSKeyValueObservingOptionNew context:nil];
}
- KVO 可以实现注册一个观察者,监听多个属性变化
keyPathsForValuesAffectingValueForKey:(NSString *)key
- 在 Person 对象里面再添加
sex和name属性 - 当
sex或者name属性改变的时候,监听age属性的观察者可以收到回调通知
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"age"]) {
NSArray *affectingKeys = @[@"sex", @"name"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- KVO 监听可变数组变化
通过常用的
addObject的方式添加不会触发回调,必须通过 mutableArrayValueForKey 方式添加
@interface Person : NSObject
@property(nonatomic,strong)NSMutableArray * dataArray;
@end
- (void)viewDidLoad {
[super viewDidLoad];
// 监听数组
[_p addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew context:nil];
}
// 修改值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 错误方式,不会触发监听回调
[self.p.dataArray addObject:@"1"];
// 正确方式,值改变收到回调
[[self.p mutableArrayValueForKey:@"dataArray"] addObject:@"1"];
}
KVO 原理探索
- 注册观察者
@interface Person : NSObject
@property(nonatomic,copy)NSString * age;
@end
@implementation Person
@end
- (void)viewDidLoad {
[super viewDidLoad];
Person * p = [[Person alloc]init];
_p = p;
NSLog(@"添加观察者之前======实例p类名:%s",object_getClassName(_p));
// 添加观察者
[_p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"添加观察者之后======实例p类名:%s",object_getClassName(_p));
}
// 观察者属性改变回调
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context {
NSLog(@"监听到改变====object:%@==keyPath:%@===change:%@",context,keyPath,change);
}
// 通过setAge的方法改变age的值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int i = 0;
i++;
self.p.age = [NSString stringWithFormat:@"%d",i];
}
- 运行结果
- 根据结果可以看到在添加观察者之后 p 对象的指针
isa由Person指向了NSKVONotifying_Person
- 利用 runtime 打印
NSKVONotifying_Person父类
Class cls = class_getSuperclass(NSClassFromString(@"NSKVONotifying_Person"));
NSLog(@"NSKVONotifying_Person父类:%@",NSStringFromClass(cls));
可以看到NSKVONotifying_Person是Person的派生类
生成这个中间类是干什么的?
通过 runtime 打印出NSKVONotifying_Person类的所有方法
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[I];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
// 调用该方法
[self printClassAllMethod:objc_getClass("NSKVONotifying_Person")];
从打印结果看到有 4 个方法,setAge、class、dealloc、_isKVOA
打印出 Person 方法
可以看到两个对象方法地址不同,说明 NSKVONotifying_Person重写了父类Person的setAge方法,同时重写了基类NSObject的 class、dealloc、_isKVOA方法
移除观察者之后,isa 会发生什么?
通过打印结果看到,移除观察者之后,isa指针由NSKVONotifying_Person又指向了Person,那么问题来了,中间类NSKVONotifying_Person还存在吗
通过runtime打印出内存中 Person 类情况
通过结果看到,移除观察者之后,中间类没有被销毁,还存在内存中,方便重用
总结
- 添加观察者,监听 Person 对象的 age 属性之后,Person 对象的指针
isa会指向派生类NSKVONotifying_Person - 当属性发生变化之后,NSKVONotifying_Person set 方法里面会调用
willChangeValueForKey和didChangeValueForKey方法触发监听的回调 - 中间类重写了被观察属性的
setter方法、class、dealloc、_isKVO方法 - 中间类在移除观察者之后,并不会立即被销毁
- KVO 只对属性产生观察,不会观察成员变量,因为成员变量没有
setter方法
拓展
- 系统观察者流程繁琐,而且必须是添加与移除
成对出现,可以再做一些逻辑优化 - 通过自定义方式观察者,把添加与移除放在 map 里面匹配,避免不成对出现
- 通过结合 Block 的回调,实现步骤简洁