前言
什么是KVO(Key-Value Observing)
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
键值观察是一种机制,它允许对象在其他对象的指定属性发生更改时收到通知。
KVO基础
KVO
从日常的开发中看出无非就是三个api
- (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;
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
那么接下来就具体看看这几个API到底有何作用。
1、NSKeyValueObservingOptions 的作用。
NSKeyValueObservingOptionOld
和 NSKeyValueObservingOptionNew
是我们常用的两个选选项。
下面通过一个 demo
来验证这个到底有什么作用
先准备如下一份代码
@interface CDPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, copy) NSArray *array;
@end
///实现如下一份代码
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [CDPerson alloc];
self.person.nick = @"Hello";
[self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionOld) context:NULL];
/// [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
/// [self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionPrior) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"change = %@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.nick = [self.person.nick stringByAppendingString:@"+"];
}
这时候我们分别监听几个不同的 options
,可以得到如下的结果
- NSKeyValueObservingOptionOld
change = {
kind = 1;
old = Hello;
}
- NSKeyValueObservingOptionNew
change = {
kind = 1;
new = "Hello+";
}
- NSKeyValueObservingOptionPrior
change = {
kind = 1;
notificationIsPrior = 1;
}
change = {
kind = 1;
}
2、 context
上下文。这种设计在很多场景都有实用,特别是在CF
、CG
等框架的时候。而从官方文档上来看就是 :
一种更安全、更可扩展的方法是使用上下文来确保您收到的通知是发送给您的观察者而不是超类的。
那么我们来验证一下
static void * personName = @"personName";
/// 2、验证 context
[self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:personName];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == personName) {
NSLog(@"%@", context);
}
NSLog(@"change = %@", change);
}
打印结果如下:
2021-07-29 23:02:04.270060+0800 001---KVO初探[10373:973646] change = {
kind = 1;
new = "Hello+";
}
2021-07-29 23:02:04.270130+0800 001---KVO初探[10373:973646] personName
2021-07-29 23:02:04.270192+0800 001---KVO初探[10373:973646] change = {
kind = 1;
new = "niubi-";
}
通过结果我们发现,这个context
确实可以被带到通知里面去。这样我们就可以更加好判断谁监听的谁。也可以保证在移除观察者的时候不会出现问题(不会把父类相同的监听给移除了)。
// 这样,即使父类也有一个观察了name 的观察者,只要context 不一样,就不会随意的移除掉。
[self.person removeObserver:self forKeyPath:@"name" context:personName]
3、要不要移除观察者
通常来说,我们注册的观察者一旦执行了 dealloc
以后,那么被观察的对象也就释放了。所以移除与否都没有关系。但是有一些情况是,虽然我的观察者释放了,但是这个被观察的对象依然还存在,那这个时候在给这个观察者发生通知那就会出问题了。比如我们上面的被观察的对象是个单列,或者其他一些暂时没办法释放的东西,那么下次在给当前对象发生通知就会触发野指针而崩溃。
所以,最好还是在我们观察者 dealloc
的时候,执行 remove
。
4、手动和自动监听KVO
在api
里面还有一个 +automaticallyNotifiesObserversForKey
:方法,这个方法默认返回 true
。也就是默认开启自动发送通知,如果我们返回 false
那么久没发自动发送通知,需要手动发送通知,即调用 willChangeValueForKey:
and didChangeValueForKey:
者两个方法来手动发出通知。也可以通过 + (BOOL)automaticallyNotifiesObserversOfName
这个方法来指定某个属性是和否可以自动发出通知(这个要在automaticallyNotifiesObserversForKey:
没有重写的情况下)。
// 自动开关关闭
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return false;
}
当我们重写了如上的方法后,整个类的KVO
就不会自动触发通知的发送。这个时候就需要手动去触发:
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
5、监听集合类型
如果我们要监听集合类型的属性(如:NSArray
),那么我们实现如下监听。
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"array" options:(NSKeyValueObservingOptionNew) context:NULL];
如果直接改变数组的成员是不会触发的,只有按照KVC
的方式去触发才可以触发通知的发送。
/// 这样是不会生效的
[self.person.dateArray addObject:@"222"];
/// 需要下面这样
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"222"];
[[self.person mutableArrayValueForKey:@"array"] addObject:@"333"];
// 亦或者
[[self.person mutableArrayValueForKey:@"dateArray"] removeObject:@"2"];
[[self.person mutableArrayValueForKey:@"array"] removeObject:@"3"];
当然这样执行集合类型的观察在配合 options
可以看看是什么效果,阁下可以自己去尝试看看结果是如何的。笔者这里就不在细说,还有包括KVC
的相关的一些对应的情况,可以查阅笔者关于KVC 的表述
6、监听keyPath 多级路径
self.person.st = [LGStudent alloc];
self.person.st.name = @"student";
[self.person addObserver:self forKeyPath:@"st.name" options:(NSKeyValueObservingOptionNew) context:NULL]
//执行如下方法
self.person.st.name = [self.person.st.name stringByAppendingString:@"+"];
///打印结果如下:
change = {
kind = 1;
new = "student+";
}
change = {
kind = 1;
new = "student++";
}
KVO 实现
KVO
到底是如何实现的,接下来我们就去探索。这里借助LLDB
和 api
来一起验证。
1、探索isa
Automatic key-value observing is implemented using a technique called isa-swizzling.
从官方文档来看,自动KVO
是一种isa-swizzling
,那么我们就先来看看这个isa
到底是什么,如下实现一段代码,并且下一个断点,分别在添加观察者和添加后打印结果
从结果我们可以看出,在添加了观察者后,isa
指向了一个 名为 NSKVONotifying_LGPerson
的类。那么这个类和我们的 LGPerson
有什么关系呢?那么结合我们前面类的原理里面探索的,类结构的第二个成员变量是 superClass
,可以得出他们是父子关系。
(lldb) po 0x00000001c28f8628
NSObject
(lldb) po 0x0000000104a55650
LGPerson
7、NSKVONotifying_CDPerson 里面有什么东西<成员变量、方法、协议>
这里笔者采用api来看看当前这个类里面到底有什么。
接下来调用如下一个方法来探索这个类里面有什么成员。
- (void)getAllMethodFromCls:(Class)cls {
unsigned int count;
Method *ms = class_copyMethodList(cls, &count);
NSLog(@"**************** 方法: %@ : %d ****************", cls, count);
for (int i = 0; i < count; i++) {
SEL sel = method_getName(ms[I]);
NSLog(@"SEL = %@", NSStringFromSelector(sel));
}
Ivar *ivs = class_copyIvarList(cls, &count);
NSLog(@"**************** 成员变量: %@ : %d", cls, count);
for (int i = 0; i < count; i++) {
const char *cName = ivar_getName(ivs[I]);
NSLog(@"Name = %@", [NSString stringWithCString:cName encoding:NSUTF8StringEncoding]);
}
objc_property_t *ps = class_copyPropertyList(cls, &count);
NSLog(@"**************** 属性: %@ : %d", cls, count);
for (int i = 0; i < count; i++) {
const char *cName = property_getName(ps[I]);
NSLog(@"Name = %@", [NSString stringWithCString:cName encoding:NSUTF8StringEncoding]);
}
NSLog(@"\n\n");
}
然后在监听前后监听后分别查看这个类的相关信息
[self getAllMethodFromCls:object_getClass(self.person)];
[self.person addObserver:self forKeyPath:@"nick" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"st.name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self getAllMethodFromCls:object_getClass(self.person)];
这里笔者有个问题是设个 st.name 到底是在何处监听的?
从结果我们可以看到,并没有setsSt.name 这样的方法。只有一个 setSt:的方法,这就让我怀疑是不是 LGStudent 也有创建了一个动态了的类,而这种多级监听最后只是通过kvc 传递到了里面相关的对象里面去了。
通过调试我发现确实是这样的,LGStudent 耶动态生成了一个 NSKVONotifying_LGStudent 子类。
(lldb) po object_getClass(self.person.st)
NSKVONotifying_LGStudent
结论
经过前面这么多分析,KVO 的大致流程和原理我们野梳理的差不多了。
1、动态注册子类 NSKVONotifying_XXX。
2、判断当前是否是属性(因为需要重写setter: 方法)。
3、修改当前对象isa指针指向动态子类NSKVONotifying_XXX。
4、调用setter 方法,并且转发给父类同时发出通知通知观察者observeValueForKeyPath: ofObject: change: context:。
5、在调用removeObserver:forKeyPath: 后有将isa 指回原来的类。