目录
一、KVO是什么
二、怎么使用KVO
三、KVO的底层实现
四、KVO常见面试题
一、KVO是什么
KVO全称Key-Value Observing,翻译过来是键值观察,是一种用来观察某个对象属性值变化的机制。
二、怎么使用KVO
使用KVO只需要抓住三个关键词就可以了:被观察者是谁——即想要观察哪个对象哪个属性值的变化;观察者是谁——即想要让谁来观察,确定后就可以给对象添加KVO和移除对象的KVO了;观察者的回调方法——即当对象的属性值发生变化后要触发的方法。
举个简单例子:
假设我们有一个Person
类,现在想要观察某个Person
对象age
属性值的变化。
// Person.h
#import
@interface INEPerson : NSObject
@property (nonatomic, assign) NSInteger age;
@end
又有一个ViewController
类,我们想要让ViewController
来观察Person
对象age
属性值的变化,确定好观察者后,就可以给对象添加KVO和移除对象的KVO了。
// ViewController.m
#import "ViewController.h"
#import "INEPerson.h"
@interface ViewController ()
@property (nonatomic, strong) INEPerson *person;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[INEPerson alloc] init];
self.person.age = 25;
// 给person对象添加KVO
[self.person addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) context:nil];
}
- (void)dealloc {
// 移除person对象的KVO
[self.person removeObserver:self forKeyPath:@"age"];
}
@end
方法解释:
/** * 给对象添加KVO * * @param observer 观察者 * @param keyPath 观察者想要观察对象哪个属性值的变化 * @param options 我们想要得到该属性变化后的新值还是旧值(如果想在添加观察者后,立即触发一次观察者的回调方法,可以在这里添上NSKeyValueObservingOptionInitial这个值) * @param context 额外信息,通常填nil */ - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
/** * 移除对象的KVO * * @param observer 观察者 * @param keyPath 观察者观察的属性 */ - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
然后我们要为观察者实现一个回调方法,以便被观察对象的属性值发生变化后,观察者能够及时收到回调并做自定义的处理。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"观察到%@对象的%@属性的值发生了变化:%@", object, keyPath, change);
}
方法解释:
/** * 观察者的回调方法 * * @param keyPath 观察者观察的属性 * @param object 观察者观察的属性所属的对象 * @param change 属性变化后的新值还是旧值都存在这里(我们还可以通过NSKeyValueChangeKindKey来判断新旧值的变化是重设、新增、替换还是移除) * @param context 额外信息,addObserver方法传过来是啥这里就是啥,通常是nil */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary
*)change context:(void *)context;
经过以上对三个关键词的捕捉与实现,我们就完成了KVO的使用。现在模拟修改一下被观察对象的age
属性。
// ViewController.m
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.person.age = 26;
}
可以看到控制台打印如下,这说明观察者的回调方法被成功触发了。
// 控制台打印
观察到对象的age属性的值发生了变化:{
kind = 1;
new = 26;
old = 25;
}
三、KVO的底层实现
继续上面的例子:
现在我们创建两个Person
对象,并且给person1
添加KVO,person2
不添加KVO。
// ViewController.m
#import "ViewController.h"
#import "INEPerson.h"
@interface ViewController ()
@property (nonatomic, strong) INEPerson *person1;
@property (nonatomic, strong) INEPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[INEPerson alloc] init];
self.person1.age = 25;
// 给person对象添加KVO
[self.person1 addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
self.person2 = [[INEPerson alloc] init];
self.person2.age = 26;
}
- (void)dealloc {
// 移除person对象的KVO
[self.person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"观察到%@对象的%@属性的值发生了变化:%@", object, keyPath, change);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.person1.age = 26;
self.person2.age = 27;
}
@end
当我们点击屏幕时,控制台打印如下。
// 控制台打印
观察到对象的age属性的值发生了变化:{
kind = 1;
new = 26;
old = 25;
}
于是我们不禁要问,点击屏幕,person1
和person2
同样是调用setAge:
方法,为什么person1
就能触发观察者的回调方法,person2
就不行呢?我们知道person1
和person2
的唯一区别就是person1
添加了KVO,而person2
没有添加KVO,那KVO到底对person1
做了什么?因此我们需要看看KVO的底层实现是什么,或许能找到问题的答案。
1、结论
我们不妨把结论先摆在这里,然后再去验证。
当我们给某个对象添加KVO之后,KVO的底层实现其实就是:
- 在运行时动态地创建一个类,这个类继承自原来那个类,并且把使用了KVO的对象的
isa
指针指向这个新类,也就是说这个对象其实已经不是原来那个类的实例了,而是新类的实例。- 然后这个新类还会重写原来类和
NSObject
类的若干个方法,其中我们最关心的就是被观察属性的setter
方法被重写了。重写的setter
方法内部主要做了三件事:首先调用willChangeValueForKey:
方法表明将要修改属性的值,然后调用原来类的setter
方法真正去修改成员变量的值,然后再调用didChangeValueForKey:
方法表明属性的值修改完毕,而且didChangeValueForKey:
方法内部还会让观察者调用观察者的回调方法。这也就是解释了“点击屏幕,
person1
和person2
同样是调用setAge:
方法,为什么person1
就能触发观察者的回调方法,person2
就不行”,就是因为它俩已经不是同一个类了,这两个类的setAge:
方法的实现压根儿不一样。
以下是新类以及新类setter
方法的伪代码。
// NSKVONotifying_INEPerson.h
#import "INEPerson.h"
@interface NSKVONotifying_INEPerson : INEPerson
@end
// NSKVONotifying_INEPerson.m
#import "NSKVONotifying_INEPerson.h"
@implementation NSKVONotifying_INEPerson
// 我们最关心的就是被观察属性的setter方法被重写了
- (void)setAge:(NSInteger)age {
// 表明将要修改属性的值
[self willChangeValueForKey:@"age"];
// 真正去修改成员变量的值
[super setAge:age];
// 表明属性的值修改完毕
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key {
// 让观察者调用回调方法,key就是上面传进来的age
[observer observeValueForKeyPath:key ofObject:self change:@{@"new": ..., @"old": ..., ...} context:nil];
}
// 重写class方法,我们猜测的原因是苹果为了隐藏KVO的底层实现,让我们开发者感知不到这个新类的存在。这样在开发中,当我们获取某个添加了KVO的对象的类时就不会产生疑惑
- (Class)class {
return [INEPerson class];
}
- (void)dealloc {
// 做一些收尾工作
// ...
}
- (BOOL)_isKVOA {
// 是否使用了KVO
return YES;
}
@end
所以在分析KVO的面试题时,我们只需要抓住KVO这两个本质的点就行了,即:
- KVO会创建一个新类,继承自原来那个类,使用了KVO的对象会指向这个新类。
- KVO会重写被观察属性的
setter
方法,里面做了三件事,并且正是在新setter
方法里才触发了观察者的回调方法。
2、验证
- 先验证第一点。
#import
Class person1Class = object_getClass(self.person1);
Class person1SuperClass = class_getSuperclass(person1Class);
Class person2Class = object_getClass(self.person2);
Class person2SuperClass = class_getSuperclass(person2Class);
NSLog(@"person1所属的类及其父类:%@, %@", person1Class, person1SuperClass);
NSLog(@"person2所属的类及其父类:%@, %@", person2Class, person2SuperClass);
控制台打印如下。
person1所属的类及其父类:NSKVONotifying_INEPerson, INEPerson
person2所属的类及其父类:INEPerson, NSObject
可见我们给person1
添加KVO之后,系统确实创建了一个继承自INEPerson
的新类NSKVONotifying_INEPerson
,并且也确实把person1
的isa
指针指向了NSKVONotifying_INEPerson
,也就是说person1
已经不是INEPerson
的实例了,而是NSKVONotifying_INEPerson
的实例。而没添加KVO的person2
还是INEPerson
的实例,继承自NSObject
。
- 再验证第二点。
#import
- (NSArray *)instanceMethodListOfClass:(Class)cls {
NSMutableArray *instanceMethodList = [@[] mutableCopy];
unsigned int count;
// 获取类的实例方法列表
Method *methodList = class_copyMethodList(cls, &count);
for (NSInteger i = 0; i < count; i++) {
// 获取方法
Method method = methodList[i];
// 获取方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
[instanceMethodList addObject:methodName];
}
// 释放
free(methodList);
return instanceMethodList;
}
NSArray *person1MethodList = [self instanceMethodListOfClass:(object_getClass(self.person1))];
NSArray *person2MethodList = [self instanceMethodListOfClass:(object_getClass(self.person2))];
NSLog(@"person1的实例方法列表:%@", person1MethodList);
NSLog(@"person2的实例方法列表:%@", person2MethodList);
IMP person1SetAgeImp = class_getMethodImplementation(object_getClass(self.person1), @selector(setAge:));
IMP person2SetAgeImp = class_getMethodImplementation(object_getClass(self.person2), @selector(setAge:));
NSLog(@"person1 setAge:方法实现的地址:%p", person1SetAgeImp);
NSLog(@"person2 setAge:方法实现的地址:%p", person2SetAgeImp);
控制台打印如下。
person1的实例方法列表:(
"setAge:",
class,
dealloc,
"_isKVOA"
)
person2的实例方法列表:(
"setAge:",
age
)
person1 setAge:方法实现的地址:0x10d8a5688
person2 setAge:方法实现的地址:0x10d54a430
可见新类确实重写了原来类和NSObject
类的若干个方法,包括setAge:
、- class
、dealloc
、_isKVOA
四个,当然其中我们最关心的还是setAge:
方法被重写,这从person1
和person2
两者setAge:
方法实现的地址不同,可以更加确认。至于重写的setAge:
方法的内部实现,我们暂时无法看到它的源码,反编译后倒是可以看到它对应的汇编代码,但我们不一定能看得懂,所以现在只是通过现象来猜测它内部实现的伪代码。
四、KVO常见面试题
1、像person->age = 1
这样直接修改成员变量会触发KVO(的回调方法)吗?
答案:
不会。
因为触发KVO本质上是调用重写后的setter
方法内部触发的,而直接修改成员变量person->age = 1
是不会调用setter
方法的,所以不会触发。
2、如何手动触发KVO?(如何手动触发KVO观察者的回调方法?)
答案:
手动调用willChangeValueForKey:
和didChangeValueForKey:
方法。(注意KVO的API里要求这两个方法必须成对调用)
[self.person1 willChangeValueForKey:@"age"];
// 如果需要修改成员变量值的话
// self.person1->_age = 26;
[self.person1 didChangeValueForKey:@"age"];