KVC&KVO

251 阅读4分钟

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监听。