KVO实现原理
- 什么是 KVO
- KVO 基本使用
- KVO 的本质
- 总结
一 、 什么是KVO
KVO(Key-Value Observing)
键值监听,是OC
对观察者模式的实现。当被观察对象的某个属性发生变化时,观察者对象会获得通知。
二 、 KVO基本使用
使用KVO
分三个步骤:
- 注册观察者,实施监听
- 通过
adObserver:forKeyPath:context:
方法注册观察者,观察者可以接受keyPath
属性的变化事件
- 通过
- 观察者实现回调方法,处理属性发生的变化
- 在观察者中实现
observeValueForKeyPath:ofObject:change:context:
方法
- 在观察者中实现
- 移除观察者
- 当观察者不需要监听时,调用
removeObserver:forKeyPath:
方法移除KVO
;调用removeObserver
需要在观察者消失之前,否则会导致crash
- 当观察者不需要监听时,调用
@interface DJTPerson : NSObject
@property (nonatomic, assign)int age;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
DJTPerson *person1 = [[DJTPerson alloc] init];
DJTPerson *person2 = [[DJTPerson alloc] init];
person1.age = 1;
person1.age = 2;
// 添加观察者
[person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |NSKeyValueObservingOptionOld context:nil];
person1.age = 10;
person1.age = 2;
// 移除观察者
[person1 removeObserver:self forKeyPath:@"age"];
}
// KVO回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@对象的%@属性改变了,change字典为:%@",object,keyPath,change);
NSLog(@"属性新值为:%@",change[NSKeyValueChangeNewKey]);
NSLog(@"属性旧值为:%@",change[NSKeyValueChangeOldKey]);
}
@end
打印结果:
<DJTPerson: 0x600001e6eda0\>对象的`age`属性改变了,`change`字典为:{
kind = 1;
new = 10;
old = 1;
}
属性新值为:10
属性旧值为:1
三、 KVO的本质
KVO
的实现依赖于Objective-C
强大的Runtime
,从Apple官方文档解释来看,被观察对象的isa
指针会指向一个中间类,而不是原来真正的类,Apple并不希望暴露更多KVO
实现细节。
在上面代码中,person1
添加了键值监听,person2
没有,而给两个实例对象的age
赋值实质是调用了set
方法,我们在DJTPerson
类中重写set
方法
存在疑问:
既然改变person
的age
值其实是调用setAge:
方法,那person1
和person2
都调用同一个方法,按理说执行的都是相同代码,为什么person1
改变age
会跑到observeValueForKeyPath:
方法里呢?它是怎么通知的呢?
@interface DJTPerson : NSObject
@property (assign, nonatomic) int age;
@end
@implementation DJTPerson
- (void)setAge:(int)age
{
_age = age;
}
@end
本质分析:既然方法调用是一样的,说明问题是出现在person1
对象本身;在touchesBegan:
方法中,设置person1
和person2
的age
值,断点调试并查看person1
和 person2
的 isa
指针内容,person1
和person2
都是实例对象,它们的isa
指向对应的类对象,打印结果显示person1
的类对象发生了改变,变成NSKVONotifying_DJTPerson
,而不是DJTPerson
。
NSKVONotifying_DJTPerson
是如何来的呢?
其实它是在person1
添加KVO
后由Runtime
动态创建的一个类,并且是DJTPerson
的子类:
下面给出使用KVO和未使用KVO监听的对象变化:
-
未使用KVO监听的对象:
使用了KVO监听的对象:
从上图看出:person1
改变age
的值,先通过isa
找到类对象NSKVONotifying_DJTPerson
,调用这个类对象的setAge:
方法NSKVONotifying_DJTPerson的setAge:
方法中会调用 Foudation
的_NSSetIntValueAndNotify()
, 在这个函数中会做下面三件事情:
1 [self willChangeValueForKey:@"age"];
2 [super setAge:age];
3 [self didChangeValueForKey:@"age"];
而在didChangeValueForKey:
函数中,会通知监听器,某某属性发生了改变:
[observer observeValueForKeyPath:ofObject:change:context];
如果自己实现这个子类,伪代码如下:
@interface NSKVONotifying_DJTPerson : DJTPerson
@end
@implementation NSKVONotifying_DJTPerson
// 重写父类setAge方法
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 伪代码
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
//调用父类setAge方法
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
@end
person2
对象改变age
的值,由于它没有使用KVO
,它的isa
指针指向DJTPerson
类对象,所以直接调用DJTPerson
中的setAge:
方法。
本质验证
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[DJTPerson alloc] init];
self.person1.age = 1;
self.person2 = [[DJTPerson alloc] init];
self.person2.age = 2;
NSLog(@"person1添加KVO监听之前 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2)
);
NSLog(@"person1添加KVO监听之前 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]
);
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO监听之后 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2)
);
NSLog(@"person1添加KVO监听之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]
);
NSLog(@"类对象 - %@ %@",
object_getClass(self.person1),
object_getClass(self.person2)
);
NSLog(@"元类对象 - %@ %@",
object_getClass(object_getClass(self.person1)),
object_getClass(object_getClass(self.person2))
);
}
打印结果为:断点调试并通过p
命令在控制台打印方法地址内容,可以看到在添加KVO
监听之前是调用的[DJTPerson setAge:]
方法,而添加监听之后,调用的是Foundation
框架的_NSSetIntValueAndNotify
函数;
再打印一下person1
和person2
的类方法名:
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
打印结果为:发现 NSKVONotifying_DJTPerson
还重写了class
方法,我们打印
NSLog(@"person1添加KVO监听之后 - %@ %@",
[self.person1 class],
[self.person2 class]
);
打印结果为:发现都是DJTPerson
,这是Apple做的一层包装,不想把底层实现暴露给开发者,而调用runtime
中object_getClass(self.person1)
看到的才是它真正所属的类。
四、总结:
KVO
的本质
- 利用
Runtime API
动态生成一个子类,并且让instance
对象的isa
指向这个全新的子类 - 当修改
instance
对象的属性时,会调用Foundation
的_NSSetXXXValueAndNotify
函数(本文中XXX=Int
) willChangeValueForKey:
- 父类原来的
setter
didChangeValueForKey:
- 内部会触发监听器(
Observer
)的监听方法(observeValueForKeyPath:ofObject:change:context
)
直接修改成员变量会触发KVO
吗?
- 不会触发
KVO
,因为没有调用set
方法,或者说没有调用willChangeValueForKey:
和didChangeValueForKey:
如何手动触发KVO
- 手动调用
willChangeValueForKey:
和didChangeValueForKey: