iOS KVO原理探索
kvo全称Key-Value-Observing,俗称“键值监听”,可以用于对对象的某个属性值的监听。kvo是Objective-C对观察者模式的实现。由于KVO的实现机制,一般继承自NSObject的对象都默认支持KVO。
具体的使用方法
- 添加观察者的方法
[_scrollView addObserver:self
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
self:监听对象,keyPath:被监听的属性,option:需要监听的属性的选项,context:上下文、用于传递数据;
- 实现监听的方法
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"contentOffset"] && object == _scrollView) {
id newValue = change[NSKeyValueChangeNewKey];
id oldValue = change[NSKeyValueChangeOldKey];
NSLog(@"object:%@,\n keyPath: %@,\n newValue:%@,\n oldValue:%@,\n context:%@", object, keyPath, newValue, oldValue, context);
}
}
keyPath:被监听的属性,object:被监听的对象,change:属性值改变的状况。(存储着属性的新值和旧值等),context:上下文传递的值;
- 移除观察者,在不需要监听的时候一定要移除观察者,通常是在dealloc中
- (void)dealloc {
[_scrollView removeObserver:self forKeyPath:@"contentOffset"];
}
KVO内部原理实现
我们可以先看一下官方文档怎么说的:
Automatic key-value observing is implemented using a technique called isa-swizzling.
key-value自动监听(即kvo)用了名为isa-swizzling的技术
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
isa指针指向一个对象的类,这个类包含了一个方法派发表
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
被观察对象的isa指针会被修改,指向一个中间类,而非真实的类.isa指针的值并不必须反应实例对象的真实对应的类
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
因而,应该调用对象的class方法来确定实例对应的类而不是isa指针的值
从官方文档我们了解到KVO的实现使用了isa-swizzling的技术,在运行的时候被观察对象的isa指针会被修改,指向一个中间类,而不是真实的类。isa指针的值不会反应实例对象真实对应的类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
下面我们来实际探究一下KVO中isa指针指向的中间内是什么?
我们监听了UIScrollView的contentOffset属性:
[_scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
在添加观察者的下一行添加断点调试可以看到控制台的打印:
添加观察者之前
添加观察者之前截图.png
添加观察者之后
添加观察者之后.png
//添加观察者之前
(lldb) po _scrollView->isa
UIScrollView
//添加观察者之后
(lldb) po _scrollView->isa
NSKVONotifying_UIScrollView
从打印的信息中我们可以看到在添加观察者之前isa指针指向的是真实的类UIScrollView,添加观察者之后_scrollView对象的isa指针指向了NSKVONotifying_UIScrollView。我们可以得出结论:在对象添加了观察者之后对象的isa指针会指向NSKVONotifying_A的分类。
isa指针指向中间类之后了,中间类是怎么告诉我们被观察属性值的变化的了。
现在我们在VC中来观察属性n,并在点击方法中改变n的值。
@property (nonatomic, assign) NSInteger n;
[self addObserver:self
forKeyPath:@"n"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"n"]) {
NSInteger nwe = [change[NSKeyValueChangeNewKey] integerValue];
NSLog(@" ========== n: %ld ==========\n", nwe);
}
}
- (void)tapAction {
self.n += 1;
_n += 1;
}
//打印如下:
========== n: 1 ==========
当我们以点方法赋值n的时候会出发观察者方法,可以看到值的改变。但是当我们以下划线的方式改变n的值时,可以发现观察者方法并没有触发。我们知道self.n其实是调用n的setter方法。因为被观察对象的isa指针指向了NSKVONotifying_A的中间类,所以调用的setter方法会从NSKVONotifying_A中找。可知NSKVONotifying_A重新实现了setter方法。
从官方文档中我们可以知道,有两种方法可以确保被观察的属性被通知发射:
There are two techniques for ensuring the change notifications are emitted.
- 自动改变通知即实现
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key,决定是否对被观察的属性自动通知。 - 手动改变通知,在
setter中,赋值前后主动调用willChangeValueForKey和didChangeValueForKey
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL isAutomatic = NO;
if ([key isEqualToString:@"n"]) {
isAutomatic = NO;
}else {
isAutomatic = [super automaticallyNotifiesObserversForKey:key];
}
return isAutomatic;
}
当对n属性返回NO时,可以看到n的变化并不会触发观察者通知方法。
- (void)setN:(NSInteger)n {
[self willChangeValueForKey:@"n"];
_n = n;
[self didChangeValueForKey:@"n"];
}
当我们手动去调用willChangeValueForKey和didChangeValueForKey时,可以看到n的变化会触发观察者通知方法。
总结
从以上的观察我们可以得知KVO的实现原理:给一个实例对象添加观察者时,会生成该类的一个中间类NSKVONotifying_A,该类会重写被观察属性的setter方法并在赋值前后调用willChangeValueForKey和didChangeValueForKey实现被观察者值变化时的通知。