面试题引发的思考:
Q: iOS用什么方式实现对一个对象的KVO?即KVO的本质是什么?
利用RuntimeAPI动态生成一个子类,并且让instance对象的
isa
指向这个全新的子类。当修改instance对象的属性时,会调用
Foundation
的_NSSetXXXValueAndNotify
函数
a>willChangeValueForKey:
b>父类原来的setter
方法对成员变量进行赋值
c>didChangeValueForKey:
d>内部会触发监听器(observer
)的监听方法observeValueForKeyPath:ofObject:change:context:
Q: 如何手动触发KVO?
- 手动调用
willChangeValueForKey:
和didChangeValueForKey:
。
Q: 直接修改成员变量会触发KVO吗?
- 不会触发KVO。
1. KVO介绍
KVO的全称是Key-Value Observing,即“键值监听”,用于监听某个对象属性值的改变
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person1 = [[Person alloc] init];
person1.age = 10;
Person *person2 = [[Person alloc] init];
person2.age = 20;
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:nil];
person1.age = 20; // [person1 setAge:20];
person2.age = 30; // [person2 setAge:30];
[person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性发生了改变 - %@", object, keyPath, change);
}
// 打印结果
Demo[1234:567890] 监听到<Person: 0x600001608200>的age属性发生了改变 - {
kind = 1;
new = 20;
old = 10;
}
由打印结果可知:
输出的是person1
的相关内容;给person1
对象添加KVO监听后,age
属性的值发生改变时,监听者observeValueForKeyPath
的方法会被调用执行。
2. KVO的本质
(1)未使用KVO监听的对象实现流程
给person1
和person2
的属性age
赋值,都会调用相同的set
方法,而set
方法的实现也是一样的。
Q: 那为什么只会打印出person1
的相关内容?
可以猜测是跟类的对象方法没有关系,跟类对象本身有关。
我们知道instance对象的isa
指向class对象,所以:
person1
的类对象是NSKVONotifying_Person
,person2
的类对象是Person
;- 而
NSKVONotifying_Person
则是使用Runtime动态创建的一个类,是Person
的一个子类。
由上图可知:
person2
在调用setAge:
方法的时候,首先根据person2
的isa
找到Person
的class对象,然后在class对象中找到setAge:
,然后实现方法。这是未使用KVO监听的对象实现流程。
(2) 使用KVO监听的对象实现流程
根据相关资料可知:
NSKVONotifying_Person
中的setAge:
方法,调用了Fundation
框架中C语言函数_NSSetIntValueAndNotify
,其内部实现流程为:
- 首先调用
willChangeValueForKey:
方法- 然后调用父类的
setAge:
方法对成员变量进行赋值- 最后调用
didChangeValueForKey:
方法,此方法内部会触发监听器(Oberser
)的监听方法observeValueForKeyPath:ofObject:change:context:
由上图可知:
person1
在调用setAge:
方法的时候,首先根据person1
的isa
找到NSKVONotifying_Person
的class对象,然后在class对象中找到setAge:
,然后实现方法。这是使用了KVO监听的对象实现流程。
1> 验证1:NSKVONotifying_Person
的内部结构
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person1 = [[Person alloc] init];
Person *person2 = [[Person alloc] init];
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:nil];
[self printMethodNamesOfClass:object_getClass(person2)];
[self printMethodNamesOfClass:object_getClass(person1)];
}
- (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);
}
// 打印结果
Demo[1234:567890] Person - age, setAge:,
Demo[1234:567890] NSKVONotifying_Person - setAge:, class, dealloc, _isKVOA,
由打印结果可知:
NSKVONotifying_Person
中有4个对象方法,分别为 setAge:
、class
、dealloc
、_isKVOA
;证实了其内部结构。
2> 验证2:NSKVONotifying_Person
中的setAge:
方法,调用了Fundation
框架中C语言函数_NSSetIntValueAndNotify
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person1 = [[Person alloc] init];
Person *person2 = [[Person alloc] init];
NSLog(@"person1添加KVO监听之前 - %p %p",
[person1 methodForSelector:@selector(setAge:)],
[person2 methodForSelector:@selector(setAge:)]);
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person1添加KVO监听之后 - %p %p",
[person1 methodForSelector:@selector(setAge:)],
[person2 methodForSelector:@selector(setAge:)]);
[person1 removeObserver:self forKeyPath:@"age"];
}
由打印结果可知:
person1
添加KVO监听之前,person1
和person2
的setAge:
方法的地址相同;person1
添加KVO监听之后,person1
的setAge:
方法的地址发生改变。
打印结果证实了:
NSKVONotifying_Person
中的setAge:
方法,调用了Fundation
框架中C语言函数_NSSetIntValueAndNotify
。
内部实现伪代码如下:
- (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];
}
3> 验证3:NSKVONotifying_Person
会重写class
方法
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person1 = [[Person alloc] init];
Person *person2 = [[Person alloc] init];
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"%@ %@", [person1 class], [person2 class]);
NSLog(@"%@ %@", object_getClass(person1), object_getClass(person2));
[person1 removeObserver:self forKeyPath:@"age"];
}
// 打印结果
Demo[1234:567890] Person Person
Demo[1234:567890] NSKVONotifying_Person Person
由打印结果可知:
通过class
方法获取到的类为Person
,而通过runtime的object_getClass
方法获取到的类为NSKVONotifying_Person
,说明NSKVONotifying_Person
重写了class
方法
Q: 那为什么要重写class
方法呢?
很明显,苹果不想让NSKVONotifying_Person
这个类暴露出来,不希望开发者知道其内部实现,其class
方法内部实现应该是以下:
// 屏蔽内部实现,隐藏了NSKVONotifying_Person类的存在
- (Class)class {
// 1.获取类对象 2.获取类对象父类
return class_getSuperclass(object_getClass(self));
}
4> 验证4:didChangeValueForKey:
内部会触发observer
的监听方法observeValueForKeyPath:ofObject:change:context:
在Person
类中,重写willChangeValueForKey:
和didChangeValueForKey:
方法:
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
- (void)setAge:(int)age {
_age = age;
NSLog(@"setAge:");
}
- (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");
}
@end
// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *person = [[Person alloc] init];
person.age = 10;
// 给person对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[person addObserver:self forKeyPath:@"age" options:options context:nil];
person.age = 20;
[person removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性发生了改变 - %@", object, keyPath, change);
}
由打印结果可知:
didChangeValueForKey:
内部会调用observer
的监听方法observeValueForKeyPath:ofObject:change:context:
。