KVO的全称是Key-Value Observing,俗称键值监听,可以用于监听某个对象属性值的改变。
下面我们来了解一下KVO的基本使用。
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@interface ViewController ()
@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
//给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
// [self.person2 addObserver:self forKeyPath:@"age" options:options context:@"456"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object,keyPath,change,context);
}
//移除监听者
-(void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"age"];
// [self.person2 removeObserver:self forKeyPath:@"age"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.age = 20;
self.person2.age = 30;
}
@end
打印结果
监听到<Person: 0x600003f74540>的age属性值改变了 - {
kind = 1;
new = 20;
old = 1;
} - 123
上述代码中,我们为person1的对象的age属性添加了观察者,实现了- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
方法,当person1的age属性值发生变化时,在该方法中监听到我们属性值的变化。而person2没有添加观察者,属性值发生变化时,没有监听到。
探寻KVO底层实现原理
通过上述代码我们发现,一旦age属性值发生了改变,就会通知观察者,并且我们知道赋值操作都是调用了属性的set方法。我们到Person类中重写了age的set方法,观察是否是KVO在set方法内部做了一些操作。
结果是person1和person2对象调用同样的set方法,但person1对象除了调用set方法之外还会执行观察者的observeValueForKeyPath
方法。同样是调用了setAge方法,那为什么结果不一样呢?在调用方法一致的情况下,差异应该是出现在对象本身上,person1对象本身在底层肯定发生了变化,KVO应该是对person1做了一些不为人知的事情。下面我们就去探索一下究竟发生了什么变化。
本质分析
首先我们在上述代码touchedBegan
方法中,给age赋值的代码打上断点,观察一下person1在addObserver后,发生了什么改变,我们通过打印isa指针,如下图所示
通过上图我们发现,person1对象执行addObserver操作之后,person1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifying_Person类对象,并且NSKVONotifying_Person类的superclass指针指向Person类,说明NSKVONotifying_Person类为Person类的子类,而person2对象没有发生任何改变。也就是说一旦person1对象添加KVO监听之后,其isa指针就会发生变化,因此set方法的执行效果就不一样了。
我们来看两张图,分别代表person2和person1在内存中存储的情况,用来做对比。
图2.未使用KVO监听的对象
图3.使用了KVO监听的对象
首先分析未使用KVO的对象person2,其isa指向的是类对象Person,Person中保存着person2的set和get方法,当对person2的age赋值时,直接找到Person类中保存的setAge方法进行调用,不会监听到值的变化。
而使用了的KVO的person1对象,其isa指向了一个新的类NSKVONotifying_Person,新的类中也创建了setAge方法。当对person1的age赋值时,就找到NSKVONotifying_Person类中的setAge方法并调用,监听值变化是在这个方法中实现的。系统通过runtime动态的创建了NSKVONotifying_Person类,并且NSKVONotifying_Person类继承至Person类,这就是为什么person1的isa指向新的类之后,我们之前在Person中重写了set方法也同样会调用。
接下来我们只需要弄清楚,NSKVONotifying_Person类中的setAge方法具体做了哪些事情。
进过分析,NSKVONotifying_Person中的setAge方法中其实调用了 Fundation框架中C语言函数 _NSsetIntValueAndNotify
,主要是以下流程,用伪代码来做一下说明。
- 调用willChangeValueForKey:将要改变方法
- 调用父类的setAge方法对成员变量赋值
- 调用didChangeValueForKey:已经改变方法,didChangeValueForKey:中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath:ofObject:change:context方法中
// 以下都为伪代码
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
本质验证
在最开始给的实例代码中,给person1对象添加观察者之前之后分别打印person1和person2的setAge方法对应的地址。
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监听之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
打印结果
2019-06-26 17:09:00.276025+0800 KVO原理[90398:25370931] person1添加KVO监听之前 - 0x10f5a7530 0x10f5a7530
2019-06-26 17:09:02.655540+0800 KVO原理[90398:25370931] person1添加KVO监听之后 - 0x10f9023d2 0x10f5a7530
(lldb) p IMP(0x10f5a7530)
(IMP) $0 = 0x000000010f5a7530 (KVO原理`-[Person setAge:] at Person.m:13)
(lldb) p IMP(0x10f9023d2)
(IMP) $1 = 0x000000010f9023d2 (Foundation`_NSSetIntValueAndNotify)
(lldb)
添加观察者之前,两个setAge方法对应的地址是一样的,添加观察者之后person1的setAge方法地址发生了变化。打印person2的setAge方法,显示的是调用Person类中的方法;打印person1的setAge方法,此时显示是调用了Foundation中的C语言函数_NSSetIntValueAndNotify
。
Foundation框架会根据属性的类型,调用不同的方法。例如我们之前定义的int类型的age属性,调用的是_NSSetIntValueAndNotify
函数,那我们吧age的属性类型变为double
再重新打印一遍。
打印结果
2019-06-26 17:42:41.731276+0800 KVO原理[90733:25452474] person1添加KVO监听之前 - 0x1023e0500 0x1023e0500
2019-06-26 17:42:42.430932+0800 KVO原理[90733:25452474] person1添加KVO监听之后 - 0x10273b18c 0x1023e0500
(lldb) p IMP(0x1023e0500)
(IMP) $0 = 0x00000001023e0500 (KVO原理`-[Person setAge:] at Person.m:13)
(lldb) p IMP(0x10273b18c)
(IMP) $1 = 0x000000010273b18c (Foundation`_NSSetDoubleValueAndNotify)
(lldb)
我们发现调用的函数变为了_NSSetDoubleValueAndNotify
,那么我们猜测Foundation框架中应该有许多地类型的函数,通过不同的类型调用不同的函数。
通过一些反编译手段,我们可以找到Foundation框架文件,通过执行命令行查询关键字找到相关函数。
那_NSSetIntValueAndNotify
内部的调用顺序又是怎样的呢?我们继续验证。
因为不能直接看到Foundation框架中的方法实现,我们可以通过父类中重写相关方法,也能看出方法的调用顺序。
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
- (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");
}
@end
@interface ViewController ()
@property (nonatomic, strong) Person *person1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
//给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object,keyPath,change,context);
}
//移除监听者
-(void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"age"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person1.age = 20;
}
打印结果
2019-06-26 18:18:22.365864+0800 KVO原理[91127:25550178] willChangeValueForKey
2019-06-26 18:18:22.366283+0800 KVO原理[91127:25550178] setAge:
2019-06-26 18:18:22.366337+0800 KVO原理[91127:25550178] didChangeValueForKey - begin
2019-06-26 18:18:22.366449+0800 KVO原理[91127:25550178] 监听到<Person: 0x600003ee8d10>的age属性值改变了 - {
kind = 1;
new = 20;
old = 20;
} - 123
2019-06-26 18:18:22.366573+0800 KVO原理[91127:25550178] didChangeValueForKey - end
由以上打印结果可以看出,最先调用willChangeValueForKey方法,再调用setAge:方法,最后调用didChangeValueForKey已经改变方法,didChangeValueForKey中会调用监听器的监听方法。
如何证明NSKVONotifyin_Person内部结构如上图3所示呢?
我们通过runtime分别打印Person类对象和NSKVONotifying_Person类对象存储的对象方法。
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.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);
}
打印结果
2019-06-27 15:18:33.343736+0800 KVO原理[2714:27649615] Person setAge:, age,
2019-06-27 15:18:33.343855+0800 KVO原理[2714:27649615] NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,
通过上述代码的打印结果,我们发现NSKVONotifying_Person类中有4个对象方法。分别为setAge:,class, dealloc, _isKVOA。至此可以验证图上所示结构。
我们分析以下系统为什么要生成这几个方法
- setAge:方法在上面已经验证过是调用的C语言的
_NSSetIntValueAndNotify
- dealloc方法应该是在内部做一些收尾工作
- _isKVOA应该就是一个返回值为BOOL类型的函数,当实现KVO就默认返回YES
- claas方法应该就是为了屏蔽内部实现,隐藏了NSKVONotifying_Person类的存在,直接返回Person类
我们在person1添加过KVO监听之后,分别打印person1和person2对象的class可以发现他们都返回Person。
NSLog(@"%@,%@",[person1 class],[person2 class]);
打印结果 Person,Person
如果NSKVONotifying_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到NSObject,而NSObject的class的实现大致为返回自己isa指向的类,返回person1的isa指向的类那么打印出来的类就是NSKVONotifying_Person,但是Apple不希望将NSKVONotifying_Person类暴露出来,并且不希望我们知道NSKVONotifying_Person内部实现,所以在内部重写了class类,直接返回Person类,那么外界在调用person1的class对象方法时,打印是Person类。这样person1给外界的感觉person1还是Person类,并不知道NSKVONotifying_Person子类的存在。
那么我们可以猜测NSKVONotifying_Person内重写的class内部实现大致为
- (Class) class {
// 得到类对象,在找到类对象父类
return class_getSuperclass(object_getClass(self));
}
面试题
1.iOS用什么方法实现对一个对象的KVO?(KVO的本质是什么?)
利用Runtime动态的生成一个instance对象原本类的子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSet***ValueAndNotify函数
- willChangeValueForKey:
- 调用父类原来的set方法
- didChangeValueForKey:
- 内部会出发监听器(Observer)的监听方法observeValueForKeyPath:ofObject:change:context
2.如何手动触发KVO?
想手动触发KVO,我们需要自己调用willChangeValueForKey:和didChangeValueForKey:两个方法,两个方法缺一不可。
代码验证
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
//给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
[self.person1 removeObserver:self forKeyPath:@"age"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object,keyPath,change,context);
}
打印结果
2019-06-27 16:06:59.103817+0800 KVO原理[3176:27785539] 监听到<Person: 0x6000024f87a0>的age属性值改变了 - {
kind = 1;
new = 1;
old = 1;
} - 123
通过打印我们可以发现,age值没有发生变化的情况下,通过手动调用willChangeValueForKey: 和didChangeValueForKey:方法,内部成功调用了observeValueForKeyPath:ofObject:change:context:方法。
3.直接修改成员变量的值会触发KVO么?
因为直接修改成员变量的值不会调用set方法,所以就不会触发KVO
网友评论