KVO是什么?
KVO全称KeyValueObserving,俗称键值监听。就是允许监听对象属性的改变,从而响应特定的事件。
KVO做了什么呢?
重写对象的setter方法吗? 我在Person类里面,重写了age的setter方法,
@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@end
#import "Person.h"
@implementation Person
- (void)setAge:(NSInteger)age {
NSLog(@"设置age: %ld", age);
_age = age;
}
@end
发现监听器的observeValueForKeyPath方法还是会执行
- (void)viewDidLoad {
[super viewDidLoad];
Person *p1 = [Person new];
p1.age = 1;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
p1.age = 10;
[p1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@改变了%@", object, keyPath, change);
}
思考是不是对对象做了一些改变,打断点打印一下它们的内存地址试试: 在person对象addObserver之前:
(lldb) po p1
<Person: 0x600003924b70>
addObserver之后
(lldb) po p1
<Person: 0x600003924b70>
没有任何变化。 在打印一下它们的isa指针试试
(lldb) po p1->isa
Person
(lldb) po p1->isa
NSKVONotifying_Person
persion对象的isa指针指向的类变化了!
底层实现分析
正常情况下,一个person实例对象,设置它的属性,会通过它的isa指针,找到它所属的类对象,通过类对象再找到它的setAge方法,然后找到方法对应的实现。
(lldb) po [p1->isa isSubclassOfClass:[Person class]]
YES
通过打印发现,NSKVONotifying_Person其实是Person类的子类,说明是runtime在运行时生成的。 所以此时的p1对象在调用setAge方法时,找到的类对象已经是NSKVONotifying_Person类了,调用的也是它里面的setAge方法。 查找网上资料可以知道,NSKVONotifying_Person中的setAge方法其实调用了Foundation框架中C语言函数 _NSSetIntValueAndNotify,_NSSetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey ,之后调用父类的setAge方法对成员变量赋值,最后调用didChangeValueForKey。didChangeValueForKey中会调用监听器的监听方法,最终来到监听这的observeValueForKeyPath方法中。
验证上述分析
我们检验一下setAge方法的内存地址
NSLog(@"添加KVO监听之前 p1 setAge = %p", [p1 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"添加KVO监听之后 p1 setAge = %p", [p1 methodForSelector:@selector(setAge:)]);
通过methodForSelector方法获取方法实现地址
2020-03-17 23:28:06.388425+0800 AwesomeOC[1599:156522] 添加KVO监听之前 p1 setAge = 0x102d71390
(lldb) p (IMP)0x102d71390
(IMP) $0 = 0x0000000102d71390 (AwesomeOC`-[Person setAge:] at Person.m:13)
2020-03-17 23:28:25.218130+0800 AwesomeOC[1599:156522] 添加KVO监听之后 p1 setAge = 0x7fff257228bc
(lldb) p (IMP)0x7fff257228bc
(IMP) $1 = 0x00007fff257228bc (Foundation`_NSSetLongLongValueAndNotify)
在获取到方法地址之后,打印方法实现发现,添加KVO之后,实现由Person类转换为了Foundation框架中的**_NSSetLongLongValueAndNotify**实现。
注意一下这里的_NSSetLongLongValueAndNotify,我现在设置的age类型是NSInteger,如果我改成double类型,那么就会变成_NSSetDoubleValueAndNotify。根据不同的属性会调用不同的函数。可以猜测还会有_NSSetBoolValueAndNotify,_NSSetFloatValueAndNotify等等这些函数。
NSKVONotifying_Person
我们通过runtime来分别打印Person类和NSKVONotifying_Person类对象内存储的实例方法。
- (void)printMethods:(Class)cls {
unsigned int count;
Method *methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
[methodNames appendFormat:@"%@ - ", cls];
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString:methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@", methodNames);
free(methods);
}
打印结果如下:
2020-03-17 23:40:29.327795+0800 AwesomeOC[1731:176338] 添加KVO监听之前 p1 setAge = 0x10de67190
2020-03-17 23:40:29.327921+0800 AwesomeOC[1731:176338] Person - age setAge:
2020-03-17 23:40:29.328166+0800 AwesomeOC[1731:176338] 添加KVO监听之后 p1 setAge = 0x7fff2572215c
2020-03-17 23:40:29.328254+0800 AwesomeOC[1731:176338] NSKVONotifying_Person - setAge: class dealloc _isKVOA
Person类中有 age 和 setAge 方法,NSKVONotifying_Person类中有四个方法:setAge,class,dealloc,_isKVOA。 现在我们可以重写屡一下他们之间的调用顺序了。
- (Class)class {
return class_getSuperclass(object_getClass(self));
}
willChangeValueForKey和didChangeValueForKey
在Person类中重写这两个方法
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey end");
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey end");
}
重新运行一下看看打印结果
2020-03-17 23:56:56.052579+0800 AwesomeOC[1813:197178] willChangeValueForKey begin
2020-03-17 23:56:56.052661+0800 AwesomeOC[1813:197178] willChangeValueForKey end
2020-03-17 23:56:56.052721+0800 AwesomeOC[1813:197178] didChangeValueForKey begin
2020-03-17 23:56:56.052906+0800 AwesomeOC[1813:197178] 监听到<Person: 0x600002a30470>的age改变了{
kind = 1;
new = 10;
old = 2;
}
2020-03-17 23:56:56.053017+0800 AwesomeOC[1813:197178] didChangeValueForKey end
通过这里的顺序可以发现,在didChangeValueForKey方法内部,调用了observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法。
所以我们是否可以手动触发KVO呢?
答案是可以的。通过手动调用willChangeValueForKey和didChangeValueForKey方法即可。
[p1 willChangeValueForKey:@“age”];
[p1 didChangeValueForKey:@“age”];
打印结果
2020-03-18 09:40:30.460369+0800 AwesomeOC[68954:1537453] 监听到<Person: 0x600002d80fe0>的age改变了{
kind = 1;
new = 2;
old = 2;
}
现在对于KVO差不多有概念了
实例对象在添加了KVO之后,runtime会在运行时创建一个继承该类的NSKVONotifying_XXX类,然后实例对象的isa指针会指向该类。那么添加KVO之后的实例对象,它的class其实已经是NSKVONotifying_XXX类,我们打印它的class之所以还是原来的类名,是因为NSKVONotifying_XXX重写了class方法。 当我们设置属性的时候,就会找到NSKVONotifying_XXX类的方法,我猜测那里面的设置属性的函数就是
- (void)setAge:(NSInteger)age {
[self willChangeValueForKey];
[super setAge:age];
[self didChangeValueForKey];
}
最终,didChangeValueForKey会回调observeValueForKeyPath。