KVC
- KVC,俗称“键值编码”,全称是“Key Value Coding”,它是一种可以直接通过字符串的名称(Key)来访问类属性的机制,而不是通过调用Setter或者Getter方法来进行访问。
- 常见的API有:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
- setValue:forkey可以给对象的所有属性赋值,但是层级只有一级,如果存在多级属性赋值,那么就需要调用多次此方法。
//新建一个Student类
@interface Student : NSObject
@property (nonatomic,assign) int height;
@end
---------------------------------------------------------------------------
//创建一个Person类有一个Student的属性
#import "Student.h"
@interface Person : NSObject
@property (nonatomic, strong) Student *stu;
@end
---------------------------------------------------------------------------
Person *p1 = [[Person alloc] init];
Student *s1 = [[Student alloc] init];
[p1 setValue:s1 forKey:@"stu"];
[s1 setValue:@20 forKey:@"height"];
- setValue:forkeyPath:支持一级属性赋值,也支持多级属性赋值,需要将属性的具体访问路径传递过去,在上文的例子中,通过stu.height就可以修改student对象的height属性。在使用上更加简洁。
[p1 setValue:@33 forKeyPath:@"stu.height"];
KVC的实现原理
- setValue:forKey:的原理
+ (BOOL)accessInstanceVariablesDirectly的返回值默认为YES。- valueForKey:的原理
KVO
- KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
KVO的实现原理
- 运行程序进入到断点,通过打印数据发现
(lldb) po self.p1
<Person: 0x60000033c620>
(lldb) po self.p2
<Person: 0x60000033c600>
(lldb) po self.p1->isa
NSKVONotifying_Person
(lldb) po self.p2->isa
Person
- 这时候会发现添加了Observer后的self.p1对象的isa指针不是指向Person,而是指向一个新的类对象NSKVONotifying_Person,而self.p2对象由于没有添加Observer,所以它的isa指针指向的是类对象Person。
由于我们并没有创建过NSKVONotifying_Person类,所以NSKVONotifying_Person是在运行时动态生成的一个新的类,新类生成之后,又将self.p1的isa指针指向了新的类对象。
为了了解NSKVONotifying_Person的内部构造,我们自定义一个方法来打印Class的方法列表和superClass
- (void)classInfo:(id)obj
{
Class objClass = object_getClass(obj);
Class superClass = class_getSuperclass(objClass);
NSLog(@"class:%@----superClass:%@",objClass,superClass);
unsigned int outCount;
Method *methods = class_copyMethodList(objClass, &outCount);
for (int i = 0; i < outCount; i ++) {
Method me = methods[i];
NSLog(@"method:%@",NSStringFromSelector(method_getName(me)));
}
free(methods);
}
- 执行上图中的程序,得到打印结果
KVO[8996:3547641] class:NSKVONotifying_Person----superClass:Person
KVO[8996:3547641] method:setAge:
KVO[8996:3547641] method:class
KVO[8996:3547641] method:dealloc
KVO[8996:3547641] method:_isKVOA
KVO[8996:3547641] class:Person----superClass:NSObject
KVO[8996:3547641] method:age
KVO[8996:3547641] method:setAge:
-
从打印结果中可以看出,p1对象由于加了KVO监听,所以它的类对象变成了NSKVONotifying_Person,而NSKVONotifying_Person对象的superClass是Person,说明NSKVONotifying_Person是Person的子类。
在NSKVONotifying_Person实例方法列表中主要有4个方法,setAge:、class、dealloc和_isKVOA,下面我们就来一一分析这四个方法。 -
NSKVONotifying_Person重写了父类中的setAge:方法,在setAge:方法中调用了Foundation框架中的_NSSetXXXValueAndNotify方法,而_NSSetXXXValueAndNotify方法就执行了监听KVO的核心逻辑,由于Person的age属性是int,所有调用_NSSetIntValueAndNotify,伪代码如下:
- (void)setAge:(int)age{
//调用Foundationf框架中的_NSSetIntValueAndNotify方法
[self _NSSetIntValueAndNotify];
}
- (void)_NSSetIntValueAndNotify{
//将要修改age的值
[self willChangeValueForKey:@"age"];
//调用父类的setAge方法去修改age的值
[super setAge:age];
//完成修改age的值,并且执行observeValueForKeyPath方法
[self didChangeValueForKey:@"age"];
}
- 通过观察上面的打印数据
(lldb) po self.p1 <Person: 0x60000033c620>可以发现NSKVONotifying_Person会重写父类的class方法,原因是Apple不想让调用者知道NSKVONotifying_Person这个中间类的存在,所以重写class,返回原类的class对象,伪代码如下
- (Class)class{
return [Person class];
}
- 当NSKVONotifying_Person类被销毁的时候,dealloc方法就被用来做一些收尾工作
- _isKVOA则是用来标识当前类是否是通过runtime动态生成的类对象,如果是,就返回YES,不是,则返回NO
还原NSKVONotifying_Person对象的内部构造
- 由于NSKVONotifying_Person是Class类型的对象,所以它内部肯定拥有isa指针和superClass指针,由此可以得到NSKVONotifying_Person的结构如下:
- 继而可以猜测出NSKVONotifying_Person的实现代码:
@interface NSKVONotifying_Person : Person
@end
@implementation NSKVONotifying_Person
- (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 [Person 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和KVO的联系
通过对KVO的探索,我们知道,给对象的某个属性添加KVO监听,其实是动态创建了一个此类的子类,然后将对象的isa指针指向新生成的类,最后通过重写属性的setter方法来添加监听。那么如果使用KVC来对属性或者成员变量进行赋值,会触发KVO监听吗?我们通过一个简单的例子来测试一下
- (void)viewDidLoad{
[super viewDidLoad];
self.p1 = [[Person alloc] init];
[self.p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.p1 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(@"observeValueForKeyPath---change:%@",change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.p1 setValue:@18 forKey:@"age"];
[self.p1 setValue:@180 forKey:@"_height"];
}
- 运行代码,点击屏幕可以看到如下打印信息
KVO[9760:3590851] observeValueForKeyPath---change:{
kind = 1;
new = 18;
old = 20;
}
KVO[9760:3590851] observeValueForKeyPath---change:{
kind = 1;
new = 180;
old = 0;
}
- 由此可知通过KVC不管是设置属性的值还是成员变量的值,都会触发KVO监听,说明在KVC内部确实会在给属性或成员变量赋值的时候,会通过类似调用didChangeValueForKey方法来触发KVO监听。