KVO
什么是KVO
KVO的全称是key-value Observng,也叫做“键值监听”,通常用来监听某个对象的某个属性值的变化。下面使用一个简单的例子来回顾一下KVO的用法。
- 创建一个XLPerson类,内部创建一个name和age属性
@interface XLPerson : NSObject
@property(nonatomic, copy)NSString *name;
@property(nonatomic, assign)int age;
@end
- 在ViewController中监听XLPerson的age属性
@implementation ViewController
- (void)viewDidLoad{
[super viewDidLoad];
self.person = [[XLPerson alloc] init];
self.person.age = 10;
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"\n keyPath:%@, \n object:%@, \n change:%@", keyPath, object, change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.age = 20;
}
@end
- person对象的age属性初始时设置为10,当点击屏幕,设置age为20时,系统自动触发了observeValueForKeyPath,打印出了age的旧值和新值,如下
2019-11-13 14:52:49.960452+0800 TestFont[52476:1429894]
keyPath:age,
object:<XLPerson: 0x6000039bec00>,
change:{
kind = 1;
new = 20;
old = 10;
}
KVO内部实现
结合之前对NSObject底层的学习我们知道,实例对象的isa指针指向它的类对象,那么上文例子中的person对象的isa指针应该指向它的类对象XLPerson,为了做对比,我们增加一个person2对象:
- (void)viewDidLoad{
[super viewDidLoad];
self.person = [[XLPerson alloc] init];
self.person.age = 10;
self.person2 = [[XLPerson alloc] init];
self.person2.age = 30;
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"\n keyPath:%@, \n object:%@, \n change:%@", keyPath, object, change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.age = 20;
}
@end
在touchesBegan方法中添加断点,然后我们使用LLDB命令来对代码进行调试
(lldb) p self.person->isa
(Class) $1 = NSKVONotifying_XLPerson
(lldb) p self.person2->isa
(Class) $2 = XLPerson
(lldb)
这时候会发现添加了Observer后的person对象的isa指针不是指向XLPerson,而是指向一个新的类对象NSKVONotifying_XLPerson,而person2对象由于没有添加Observer,所以它的isa指针指向的是类对象XLPerson。
由于我们并没有创建过NSKVONotifying_XLPerson类,所以NSKVONotifying_XLPerson是在运行时动态生成的一个新的类,新类生成之后,又将person的isa指针指向了新的类对象。
为了了解NSKVONotifying_XLPerson的内部构造,我们自定义一个方法来打印Class的方法列表和superClass
- (void)descriptionOfClass:(Class)cls{
NSLog(@"------------ %@ -----------", NSStringFromClass(cls));
NSLog(@"%@ superClass ----> %@", NSStringFromClass(cls),NSStringFromClass(class_getSuperclass(cls)));
unsigned int count;
Method *methondList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = methondList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"%@ ----> %@", NSStringFromClass(cls), methodName);
}
free(methondList);
}
修改示例中的代码,打印出person和person1的方法列表和superClass
- (void)viewDidLoad{
[super viewDidLoad];
self.person = [[XLPerson alloc] init];
self.person.age = 10;
self.person2 = [[XLPerson alloc] init];
self.person2.age = 20;
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self descriptionOfClass:object_getClass(self.person)];
NSLog(@"\n");
[self descriptionOfClass:object_getClass(self.person2)];
}
注意:由于对象的实例方法都存放在类对象的methonList中,所以此处我们需要通过object_getClass方法拿到person和person1对象的类对象,然后通过遍历类对象的方法列表打印出具体的方法名称。 object_getClass方法如果传递过去一个示例对象,那么会返回对应的类对象,如果传递过去一个类对象,会返回对应的元类对象。
运行程序,得到以下运行结果
从图中可以看出,person对象由于加了KVO监听,所以它的类对象变成了NSKVONotifying_XLPerson,而NSKVONotifying_XLPerson对象的superClass是XLPerson,说明NSKVONotifying_XLPerson是XLPerson的子类。
在NSKVONotifying_XLPerson方法列表中主要有4个方法,setAge:、class、dealloc和_isKVOA,下面我们就来一一分析这四个方法。
- NSKVONotifying_XLPerson重写了父类中的setAge:方法,在setAge:方法中调用了Foundation框架中的_NSSetIntValueAndNotify方法,而_NSSetIntValueAndNotify方法就执行了监听KVO的核心逻辑,伪代码如下:
- (void)setAge:(int)age{
//调用Foundationf框架中的_NSSetIntValueAndNotify方法
[self _NSSetIntValueAndNotify];
}
- (void)_NSSetIntValueAndNotify{
//将要修改age的值
[self willChangeValueForKey:@"age"];
//调用父类的setAge方法去修改age的值
[super setAge:age];
//完成修改age的值,并且执行observeValueForKeyPath方法
[self didChangeValueForKey:@"age"];
}
- NSKVONotifying_XLPerson会重写父类的class方法,原因是Apple不想让调用者知道NSKVONotifying_XLPerson这个中间类的存在,所以重写class,返回原类的class对象,伪代码如下
- (Class)class{
return [XLPerson class];
}
- 当NSKVONotifying_XLPerson类被销毁的时候,dealloc方法就被用来做一些收尾工作
- _isKVOA则是用来标识当前类是否是通过runtime动态生成的类对象,如果是,就返回YES,不是,则返回NO
还原NSKVONotifying_XLPerson对象的内部构造
上文介绍了NSKVONotifying_XLPerson对象中的几个主要的方法,现在我们就来还原一下NSKVONotifying_XLPerson对象完整的内部结构。
首先,NSKVONotifying_XLPerson是Class类型的对象,所以它内部肯定拥有isa指针和superClass指针,由此可以得到NSKVONotifying_XLPerson的结构如下:
结合isa指针的指向可以得到以下结构:
由此也可以得到NSKVONotifying_XLPerson的伪代码如下
@interface NSKVONotifying_XLPerson : XLPerson
@end
@implementation NSKVONotifying_XLPerson
- (void)setAge:(int)age{
//调用Foundationf框架中的_NSSetIntValueAndNotify方法
[self _NSSetIntValueAndNotify];
}
- (void)_NSSetIntValueAndNotify{
//将要修改age的值
[self willChangeValueForKey:@"age"];
//调用父类的setAge方法去修改age的值
[super setAge:age];
//完成修改age的值,并且执行observeValueForKeyPath方法
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
//触发observeValueForKeyPath方法
[self observeValueForKeyPath:@"age" ofObject:self change:nil context:nil];
}
- (void)dealloc{
//释放操作
}
- (Class)class{
return [XLPerson class];
}
- (BOOL)_isKVOA{
return YES;
}
@end
KVO总结
- 首先,给一个实例对象添加KVO,内部是利用Runtime动态的生成一个此实例对象的类对象的子类,具体的格式为_NSKVONotifying_XXX,并且让实例对象的isa指针指向这个新生成的类。
- 重写属性的set方法,当调用set方法时,会调用Foundation框架的NSSetXXXValueAndNotify函数
- 在_NSSetXXXValueAndNotify中会执行一下步骤
- 调用willChangeValueForKey:方法
- 调用父类的set方法,重新赋值
- 调用didChangeValueForKey:方法,didChangeValueForKey:内部会触发监听器的observeValueForKeyPath:ofObject:change:context:方法
KVC
什么是KVC?
KVC,俗称“键值编码”,全称是“Key Value Coding”,它是一种可以直接通过字符串的名称(Key)来访问类属性的机制,而不是通过调用Setter或者Getter方法来进行访问。
KVC的常用方法如下
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
KVC有两种赋值和取值方法,下面我们通过一个简单的例子来了解一下。首先创建XLPerson类和XLStudent类
@interface XLStudent : NSObject
@property(nonatomic, assign)int num;
@end
@interface XLPerson : NSObject
@property(nonatomic, strong)XLStudent *student;
@property(nonatomic, copy)NSString *name;
@property(nonatomic, assign)int age;
@end
然后通过KVC来设置XLPerson和XLStudent的属性的值,如下
- (void)viewDidLoad{
[super viewDidLoad];
XLPerson *person = [[XLPerson alloc] init];
[person setValue:[[XLStudent alloc] init] forKey:@"student"];
[person setValue:@10 forKey:@"age"];
[person setValue:@"张三" forKey:@"name"];
[person setValue:@20 forKeyPath:@"student.num"];
NSNumber *age = [person valueForKey:@"age"];
NSString *name = [person valueForKey:@"name"];
NSNumber *num = [person valueForKeyPath:@"student.num"];
NSLog(@"%@, %@, %@",age,name,num);
}
最后得到结果age=10、name=张三、num=20,由此可见,通过KVC确实可以修改对象中的属性。
使用KVC除了可以修改属性,也可以修改成员变量的值,在XLPerson中增加如下成员变量
@interface XLPerson : NSObject{
int _height;
int _weight;
}
@end
然后使用KVC进行赋值
XLPerson *person = [[XLPerson alloc] init];
[person setValue:@30 forKey:@"_height"];
[person setValue:@40 forKeyPath:@"_weight"];
NSLog(@"%@, %@",person->_height,person->_weight);
最后可以发现KVC确实也能修改成员变量的值。
同时,通过上面的代码我们可以看出两种赋值和取值方法的区别。
- setValue:forkey可以给person对象的所有属性赋值,但是层级只有一级,如果存在多级属性赋值,那么就需要调用多次此方法,上文的例子中,如果要修改student对象的num属性,就必须调用两次
[[person valueForKey:@"student"] valueForKey:@"num"];
- setValue:forkeyPath:支持一级属性赋值,也支持多级属性赋值,需要将属性的具体访问路径传递过去,在上文的例子中,通过student.num就可以修改student对象的num属性。在使用上更加简洁。
KVC底层原理
setValue:forkey:赋值流程
其实通过setValue:forkey方法给对象的属性赋值,主要经过以下几个流程
- 首先会按照setKey:、_setKey:的顺序到对象的方法列表中寻找这两个方法,如果找到了方法,则传参并且调用方法。
- 如果没有找到方法,则通过accessInstanceVariablesDirectly方法的返回值来决定是否能够查找成员变量。如果accessInstanceVariablesDirectly返回YES,则会按照以下顺序到成员变量列表中查找对应的成员变量:
- _key
- _isKey
- key
- isKey
- 如果accessInstanceVariablesDirectly返回NO,则直接抛出NSUnknownKeyException异常。
- 如果在成员变量列表中找到对应的属性值,则直接进行赋值,如果找不到,则会抛出NSUnknownKeyException异常。
对应流程图如下:
valueForKey:取值流程
通过valueForKey:方法取值,流程如下:
- 首先会按照以下顺序查找方法列表
- getKey
- key
- isKey
- _key
- 如果找到就直接传递参数,调用方法,如果未找到则查看accessInstanceVariablesDirectly方法的返回值,如果返回NO,则直接抛出NSUnknownKeyException异常
- 如果accessInstanceVariablesDirectly方法返回YES,则按如下顺序查找成员变量列表
- _key
- _isKey
- key
- isKey
- 如果能找到对应的成员变量,则直接获取成员变量的值,如果未找到,则抛出NSUnknownKeyException异常
流程图如下:
KVC和KVO的联系
通过对KVO的探索,我们知道,给对象的某个属性添加KVO监听,其实是动态创建了一个此类的子类,然后将对象的isa指针指向新生成的类,最后通过重写属性的setter方法来添加监听。那么如果使用KVC来对属性或者成员变量进行赋值,会触发KVO监听吗?我们通过一个简单的例子来测试一下
还是使用上文的XLPerson对象
- (void)viewDidLoad{
[super viewDidLoad];
self.person = [[XLPerson alloc] init];
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.person addObserver:self forKeyPath:@"_height" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"\n keyPath:%@, \n object:%@, \n change:%@", keyPath, object, change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person setValue:@10 forKey:@"age"];
[self.person setValue:@20 forKeyPath:@"_height"];
}
运行代码,点击屏幕可以看到如下打印信息
通过KVC不管是设置属性的值还是成员变量的值,都会触发KVO监听,说明在KVC内部确实会在给属性或成员变量赋值的时候,会通过类似调用didChangeValueForKey方法来触发KVO监听。
面试题
KVO
KVO的本质是什么?
- 给一个实例对象添加KVO,系统内部是利用Runtime动态的生成一个此实例对象的类对象的子类,具体的格式为_NSKVONotifying_XXX,并且让实例对象的isa指针指向这个新生成的类。
- 重写属性的set方法,当调用set方法时,会调用Foundation框架的NSSetXXXValueAndNotify函数
- 在_NSSetXXXValueAndNotify中会执行以下步骤
- 调用willChangeValueForKey:方法
- 调用父类的set方法,重新赋值
- 调用didChangeValueForKey:方法,didChangeValueForKey:内部会触发监听器的observeValueForKeyPath:ofObject:change:context:方法
如何手动触发KVO?
在修改变量前后手动调用willChangeValueForKey:和didChangeValueForKey:方法
[self willChangeValueForKey:name];
_name = @"xxx";
[self didChangeValueForKey:name];
直接修改成员变量的值是否会触发KVO?
直接修改成员变量的值不会触发KVO,因为没有触发setter方法。
KVC
通过KVC修改属性的值会触发KVO吗?
会触发KVO。
KVC的赋值过程和取值过程分别是什么样的?
参考上文流程图
KVC的原理是什么?
参考上文流程图
结束语
以上内容纯属个人理解,如果有什么不对的地方欢迎留言指正。
一起学习,一起进步~~~