简介
KVC全称是Key-Value Coding,键值编码,可以通过Key来访问和修改Value。
常见API
- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
setValue:forKey:
根据属性名做赋值的操作
setValue: forKeyPath:
根据key的路径做多层级间的赋值操作
使用示例代码:
People *people = [People new];
Dog *dog = [Dog new];
people.dog = dog;
[people setValue:@10 forKey:@"age"];
[people setValue:@"s" forKeyPath:@"dog.name"];
[people valueForKey:@"age"];
[people valueForKeyPath:@"dog.name"];
底层原理
赋值
当我们调用setValue:forKeyPath:
方法后,系统会按照顺序查找两个方法:setKey和_setKey,如果实现了这两个方法其中一个,那么系统就会调用对应的方法。
通过在People类中分别实现setAge和_setAge两个方法,做实验代码:
People.h
#import <Foundation/Foundation.h>
@class Dog;
@interface People : NSObject
@property(nonatomic,copy) NSString *name;
@property (nonatomic, strong) Dog *dog;
@end
People.m
@implementation People
- (void)setAge:(NSInteger)age {
NSLog(@"setAge");
}
- (void)_setAge:(NSInteger)age {
NSLog(@"_setAge");
}
- (NSInteger)age {
NSLog(@"get age");
return 0;
}
@end
实验结果:
只保留setAge方法,会输出setAge;只保留_setAge方法,会输出_setAge;当两个方法同时存在时,setAge方法优先于_setAge方法。
如果没有实现setKey
和_setKey
这两个方法的话,会继续调用accessInstanceVariablesDirectly
方法,这个方法的返回值是BOOL类型(默认返回值为YES);如果返回值是NO,系统会调用setValue:forUndefinedKey:
方法,并抛出NSUnknownKeyException
类型的异常。如果返回值是YES,那么会继续按照顺序依次查找 _key、_isKey、key、isKey
这四个成员变量,如果找到其中一个成员变量,完成赋值。如果四个成员变量全都未找到,系统会调用setValue:forUndefinedKey:
方法,并抛出NSUnknownKeyException
类型的异常。
继续在People类中实验没有实现setKey
和_setKey
方法,但存在_isKey成员变量的情况,代码如下:
#import "People.h"
@interface People ()
{
NSInteger _isAge;
}
@end
@implementation People
- (NSInteger)age {
NSLog(@"%ld",_isAge);
NSLog(@"get age");
return 0;
}
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
@end
能正确给People实例对象赋值,系统没有抛出异常。
取值
当我们调用valueforKey:
方法后,系统会按照顺序查找以下方法:getKey、key、isKey、_key,如果实现了其中一个,那么系统就会调用对应的方法。
通过在People类中分别实现getKey、key、isKey、_key方法,做实验代码:
People.m
#import "People.h"
@implementation People
- (NSInteger)getAge {
NSLog(@"getAge");
return 10;
}
- (NSInteger)age {
NSLog(@"age");
return 10;
}
- (NSInteger)isAge {
NSLog(@"isAge");
return 10;
}
- (NSInteger)_Age {
NSLog(@"_Age");
return 10;
}
- (void)setAge:(NSInteger)age {
NSLog(@"setAge");
}
@end
实验结果:
getKey、key、isKey、_key方法优先级依次降低。
如果没有实现getKey、key、isKey、_key这几个方法的话,会继续调用accessInstanceVariablesDirectly
方法,这个方法的返回值是BOOL类型(默认返回值为YES);如果返回值是NO,系统会调用valueForUndefinedKey:
方法,并抛出NSUnknownKeyException
类型的异常。如果返回值是YES,那么会继续按照顺序依次查找 _key、_isKey、key、isKey
这四个成员变量,如果找到其中一个成员变量,完成取值。如果四个成员变量全都未找到,系统会调用valueForUndefinedKey:
方法,并抛出NSUnknownKeyException
类型的异常。
KVC赋值能触发KVO么?
可以,KVC赋值的时候,应该有调用
willChangeValueForKey:
didChangeValueForKey:
通知KVO监听者值改变。
假设定义一个Student类,对age
key进行KVO监听和KVC赋值操作的关键函数进行重写打印log。
@interface Student : NSObject
@end
@implementation Student
- (void)setAge:(int)age {
NSLog(@"set age");
}
- (int)age {
NSLog(@"age");
return 0;
}
-(void)setValue:(id)value forKey:(NSString *)key {
NSLog(@"setvalue forkey");
[super setValue:value forKey:key];
}
- (id)valueForKey:(NSString *)key {
NSLog(@"value forkey");
return [super valueForKey:key];
}
- (void)willChangeValueForKey:(NSString *)key {
NSLog(@"willChangeValueForKey");
[super willChangeValueForKey:key];
}
- (void)didChangeValueForKey:(NSString *)key {
NSLog(@"didChangeValueForKey begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey end");
}
@end
@interface StudentObserver : NSObject
@end
@implementation StudentObserver
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"observeValuechange %@",change);
}
@end
当我们Student的实例对象的age
属性进行监听新旧值的改变,然后用KVC的方式对age
赋值,观察输出结果。
Student *stu = [[Student alloc] init];
StudentObserver *ob = [[StudentObserver alloc] init];
[stu addObserver:ob forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"111"];
[stu setValue:@1 forKey:@"age"];
输出结果如下:
setvalue forkey
willChangeValueForKey
value forkey
age
set age
didChangeValueForKey begin
value forkey
age
observeValuechange {
kind = 1;
new = 0;
old = 0;
}
didChangeValueForKey end
观察输出log,可以发现setValue:forKey:
方法内部,先调用了willChangeValueForKey
,然后调用了valueForKey:
获取旧值,紧接着对age
进行赋值操作,然后调用了didChangeValueForKey
方法内部开始又调用了valueForKey:
获取新值,方法最后通知监听改变。