Runtime系列导读
KVO简介
全称Key-Value Observing,KVO是Object-C中定义的一个通知机制,其定义了一种对象间监控对方状态的改变,并做出反应的机制。对象可以为自己的属性注册观察者,当这个属性的值发生了改变,系统会对这些注册的观察者做出通知。
KVO用法
添加监听
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
- observer: 观察者对象. 其必须实现方法observeValueForKeyPath:ofObject:change:context:
- keyPath: 被观察的属性,不能为nil
- options: 设定通知观察者时传递的属性值的类型,具体设置可查看枚举
NSKeyValueObservingOptions
- context: 一些其他的需要传递给观察者的上下文信息,通常设置为nil
监听实现
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
- keyPath: 被观察的属性
- object: 被观察的对象
- change: 根据options设置,可能出现old|new,或者都有
- context: 监听时传入的上下文信息
KVO实现过程
KVO的实现过程实际上是利用了OC的runtime机制,当一个实例对象添加观察者时,底层根据该实例对象所属的类动态添加了一个类(动态添加的类名就是在原来类的类名前加上NSKVONotifying_前缀),这个类是继承自原来的类的。这里以继承自NSObject的KVOTest
类来举例。
- KVOTest实现:
@interface KVOTest : NSObject
@property (nonatomic, assign) NSInteger age;
@end
@implementation KVOTest
-(void)didChangeValueForKey:(NSString *)key
{
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey:%@,%p", key, self);
}
@end
- 调用代码:
-(void)testKVO2
{
self.test = [KVOTest new];
self.test.age = 10;
NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.test addObserver:self forKeyPath:@"age" options:option context:nil];
self.test.age = 10;
}
- 监听代码:
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
NSLog(@"%@ - %@" , keyPath, change);
}
- 打印日志:
2022-06-25 21:58:32.716895+0800 StudyApp[31571:1344036] age - {
kind = 1;
new = 10;
old = 10;
}
2022-06-25 21:58:32.716960+0800 StudyApp[31571:1344036] didChangeValueForKey:age,0x600003f14590
上面实例的底层实现过程如下:
- self.test添加观察者时,底层就利用runtime动态生成一个叫NSKVONotifying_KVOTest的类,这个类继承自KVOTest类,并重写了以下实例方法:
- 重写class方法,不重写的话调用这个方法返回的是NSKVONotifying_KVOTest这个类,重写后返回的是原本的KVOTest类。苹果这么做的目的是为了隐藏KVO的实现细节。
- 重写dealloc方法,在这个方法里面做一些收尾的工作。
- 重写_isKVOA方法,这是一个私有方法,我们不必关心。
- 重写被监听属性的setter方法,上面案例只监听了name属性,所以只需重写setName:方法。重写setter是实现KVO的关键,在setter方法里面实际是调用的Foundation框架下的_NSSetValueAndNotify方法(表示不是一个固定的,这个和监听的属性的类型有关,比如是属性是int类型的话这里就是__NSSetIntValueAndNotify,所包含的类型会在后面列出来)。
- 然后将self.test这个实例对象的isa改为指向NSKVONotifying_KVOTest(原本是指向KVOTest类的)。
- 当我们设置被监听属性的值时self.test.age = 10,是调用的setAge:方法,前面说了setAge:方法被重写了,所以实际上调用的是_NSSetIntValueAndNotify这个方法。这个方法实现苹果是没有开源的,无法得知其具体实现,不过可以猜出其实现流程大致如下:
- 首先调用[self willChangeValueForKey:@"age"];这个方法。
- 然后调用原先的setter方法的实现(比如_age = age;);
- 再调用[self didChangeValueForKey:@"age"];这个方法。
- 最后在didChangeValueForKey:这个方法中调用观察者的observeValueForKeyPath: ofObject: change: context:方法来通知观察者属性值发生了变化。
KVO答疑
如何手动触发KVO?
- 手动调用willChangeValueForKey: 和 didChangeValueForKey:
直接修改成员变量会触发KVO吗?
- 不会触发KVO
对同一个属性N次注册,修改一次该属性,observeValueForKeyPath会调用几次
N次。
对同一个属性一次注册,多次removeObserver,会发生什么
crash,提示**'Cannot remove an observer <ViewController 0x7fa3fd708c60> for the key path "age" from <KVOTest 0x6000018e8790> because it is not registered as an observer.'**
网友评论