目录
- 简介
- 基本使用
- 底层本质探究
- 总结
- 其他补充
---- 验证NSKVONotifying_*子类内部方法
---- 验证_NSSetXXValueAndNotify方法内部实现
---- 手动触发KVO
简介
KVO
全称是Key-Value Observing
,俗称“键值监听”,可用于监听某个对象属性值的改变。
基本使用
添加监听:
self.person = [[Person alloc] init];
self.person.age = 10;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
监听回调方法:
// 当监听属性值发生改变时调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变 - %@", object, keyPath, change);
}
移除监听:
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}
点击事件:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person.age = 20;
}
结果打印:
监听到< Person: 0x283d468c0>的age属性值改变了 - {
kind = 1;
new = 20;
old = 10;
}
底层本质探究
首先我们思考一下,
KVO
的功能是对键值进行监听
,在数据改变的时候我们收到消息,而属性改变就是set
方法,我们从set
方法入手进行观察。
方法一通过打印来观察:
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSLog(@"添加KVO监听之前类变化 - %@ %@", object_getClass(self.person1), object_getClass(self.person2));
NSLog(@"添加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(@"添加KVO监听之后类变化 - %@ %@", object_getClass(self.person1), object_getClass(self.person2));
NSLog(@"添加KVO监听之后方法变化 - %p %p", [self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
打印结果:
添加KVO监听之前类变化 - Person Person
添加KVO监听之前方法变化 - 0x100872518 0x100872518
添加KVO监听之后类变化 - NSKVONotifying_Person Person
添加KVO监听之后方法变化 - 0x187802120 0x100872518
方法二:通过增加断点,在控制台输入LLDB命令
来观察:
添加KVO前观察person1实例对象
的isa指针
(lldb) p self.person1.isa
(Class) $2 = Person
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $3 = Person
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
添加KVO后观察person1实例对象
的isa指针
(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_Person
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = Person
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)
观察person1实例对象
的setAge:
方法地址变化
(lldb) p (IMP) 0x100872518
(IMP) $0 = 0x0000000100872518 (0x0000000100872518)
(lldb) p (IMP) 0x187802120
(IMP) $1 = 0x0000000187802120 (Foundation`_NSSetIntValueAndNotify)
(lldb)
观察结果
通过对实例对象和方法的前后打印可以看出:
实例对象
person1
添加了KVO
,它的类
和setAge:
方法的内存地址发生了改变。
实例对象person2
因为没有添加KVO
所以类和方法都没有发生改变.
结论1:验证了NSKVONotifying_Person
类的存在
给
Person
的实例对象添加KVO
后,RunTime
会在运行过程中创建一个继承于Person
的子类NSKVONOtifying_Person
,并将Person实例对象
的isa
指针指向这个子类。
结论2:验证_NSSetIntValueAndNotify
方法存在
打印结果可知,添加
KVO
后setAge:
方法的内存地址发生了改变,实现是Foundation
框架里的_NSSetIntValueAndNotify
方法。
总结
给Person
的实例对象添加KVO
后,Runtime
会在运行过程中创建一个继承于Person
的子类NSKVONOtifying_Person
,并将Person
实例对象的isa指针指向这个子类。子类内部会将被观察属性的set方法
实现替换为_NSSetIntValueAndNotify
方法,如图所示:
伪代码:
// 伪代码
- (void)setAge:(int)age {
// 调用Foundation框架里的方法
_NSSetIntValueAndNotify();
}
_NSSetIntValueAndNotify
内部会调用willChangeValueForKey:
、父类setAge:
、didChangeValueForKey:
方法:
// 伪代码
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
didChangeValueForKey
内部会调用observeValueForKeyPath:
方法:
// 伪代码
- (void)didChangeValueForKey:(NSString *)key {
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
总结:
当属性的set方法出发后,执行子类内部的
_NSSetIntValueAndNotify
方法
_NSSetIntValueAndNotify
内部调用:
willChangeValueForKey:--->super setAge:--->didChangeValueForKey:
didChangeValueForKey:
内部调用:observeValueForKeyPath:
从而完成整个KVO
监听流程。
其他补充
-
验证NSKVONotifying_*子类内部方法
[self printClassMethodList:object_getClass(self.person1)];
[self printClassMethodList:object_getClass(self.person2)];
- (void)printClassMethodList:(Class)cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [[NSMutableString alloc] init];
for (int i = 0; i < count; i++) {
Method method = methodList[i];
[methodNames appendString:NSStringFromSelector(method_getName(method))];
[methodNames appendString:@", "];
}
free(methodList);
NSLog(@"%@的方法有%@", NSStringFromClass(cls), methodNames);
}
打印结果:
NSKVONotifying_Person的方法有: setAge:, class, dealloc, _isKVOA
Person的方法有:setAge:, age
由打印结果得出,子类中不仅有set方法
,还有class、dealloc、_isKVOA
方法
-
验证_NSSetXXValueAndNotify方法内部实现
在Person类中添加打印,并重新设置age的值:
- (void)setAge:(int)age {
_age = age;
NSLog(@"setAge");
}
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey - Begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - End");
}
打印结果:
willChangeValueForKey
setAge
didChangeValueForKey - Begin
监听到<Person: 0x600000d5c270>的age属性值改变 - {
kind = 1;
new = 20;
old = 11;
}
didChangeValueForKey - End
得出_NSSetIntValueAndNotify
内部执行顺序是:
willChangeValueForKey:
setAge:
didChangeValueForKey
observeValueForKeyPath:
这里之所以调用
_NSSetIntValueAndNotify
方法是因为被添加KVO的属性是int
类型,Foundation
框架内部还有_NSSetDoubleValueAndNotify
、_NSSetRangeValueAndNotify
等方法,会根据被添加观察的属性类型决定调用哪个方法。
-
手动触发KVO
由于observeValueForKeyPath:
是在didChangeValueForKey:
方法内部调用的,所以需要调用didChangeValueForKey:
方法来手动触发,且在调用didChangeValueForKey:
前需要调用willChangeValueForKey:
方法:
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
网友评论