准备
一、 KVC 简介
什么是 KVC
KVC的全称是Key-Value Coding(键值编码),是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问,可以通过字符串来访问一个对象的成员变量或其关联的存取方法(getter or setter)。- 通常,我们可以直接通过存取方法或变量名来访问对象的属性。我们也可以使用
KVC间接访问对象的属性,并且KVC还可以访问私有变量。某些情况下,KVC还可以帮助简化代码。
访问对象属性
1. 常用 API
- (nullable id)valueForKey:(NSString *)key; // 通过 key 来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; // 通过 keyPath 来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; // 通过 key 来赋值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通过 keyPath 来赋值
2. 基础操作
SSLPerson、SSLPackage的声明:
@interface SSLPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) SSLPackage *package;
@end
@interface SSLPackage : NSObject
@property (nonatomic, copy) NSNumber *money;
@end
可以通过setter方法为name属性赋值:
person.name = @"SSL";
也可以通过KVC的方式为name属性赋值:
[person setValue:@"SSL" forKey:@"name"];
3. KeyPath
KVC还支持多级访问,KeyPath用法跟点语法相同。例如:我们想对person的package属性的money属性赋值,其KeyPath为package.money。
[person setValue:@1000000 forKeyPath:@"package.money"];
4. 多值操作
给定一组Key,获得一组value,以字典的形式返回。该方法为数组中的每个Key调用valueForKey:方法。
- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
将指定字典中的值设置到消息接收者的属性中,使用字典的Key标识属性。默认实现是为每个键值对调用setValue:forKey:方法 ,会根据需要用nil替换NSNull对象。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;
二、 苹果官方文档解释 KVC
苹果官方文档 关于KVC的解释:
About Key-Value Coding
Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key- value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.
You typically use accessor methods to gain access to an object’s properties. A get accessor (or getter) returns the value of a property. A set accessor (or setter) sets the value of a property. In Objective-C, you can also directly access a property’s underlying instance variable. Accessing an object property in any of these ways is straightforward, but requires calling on a property-specific method or variable name. As the list of properties grows or changes, so also must the code which accesses these properties. In contrast, a key-value coding compliant object provides a simple messaging interface that is consistent across all of its properties.
Key-value coding is a fundamental concept that underlies many other Cocoa technologies, such as key-value observing, Cocoa bindings, Core Data, and AppleScript-ability. Key-value coding can also help to simplify your code in some cases.
三、 KVC 设值和取值过程
KVC 设值过程
打开 苹果官方文档 查看KVC的设值原理:
Search Pattern for the Basic Setter
The default implementation of
setValue:forKey:, givenkeyandvalueparameters as input, attempts to set a property namedkeytovalue(or, for non-object properties, the unwrapped version ofvalue, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:
- Look for the first accessor named
set<Key>:or_set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.- If no simple accessor is found, and if the class method
accessInstanceVariablesDirectlyreturnsYES, look for an instance variable with a name like_<key>,_is<Key>,<key>, oris<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.- Upon finding no accessor or instance variable, invoke
setValue:forUndefinedKey:. This raises an exception by default, but a subclass ofNSObjectmay provide key-specific behavior.
- 查看文档可以看到,
KVC的设值过程有三步,下面进行分析。
1. set<Key>: / _set<Key>
按照set<Key>:、_set<Key>:顺序查找方法。
如果找到就调用并将value传进去,否则执行下一步,接下来用示例进行验证。
创建SSLPerson类,实现setName:、_setName:方法:
@interface SSLPerson : NSObject
@end
@implementation SSLPerson
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name {
NSLog(@"%s - %@",__func__,name);
}
@end
KVC设值代码调用:
SSLPerson *person = [[SSLPerson alloc] init];
[person setValue:@"SSL" forKey:@"name"];
查看结果:
-[SSLPerson setName:] - SSL
setName:被成功调用,_setName:没有被调用,说明这两个方法是不会重复调用的。- 注意:如果声明了
name属性,即使setName:没有实现,_setName也是不会被调用的。
去掉setName:方法:
@interface SSLPerson : NSObject
@end
@implementation SSLPerson
- (void)_setName:(NSString *)name {
NSLog(@"%s - %@",__func__,name);
}
@end
再次运行程序,可以看到_setName:方法被调用了:
-[SSLPerson _setName:] - SSL
注:测试的时候发现
set<IsKey>:也是可以被调用的。
2. _<key>, _is<Key>, <key>, or is<Key>
查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照_<key>、_is<Key>、<key>、is<Key>顺序查找成员变量。如果找到就将value赋值给它,否则执行下一步。如果+accessInstanceVariablesDirectly方法返回NO也执行下一步。
创建测试代码:
@interface SSLPerson : NSObject{
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
@implementation SSLPerson
@end
- (void)viewDidLoad {
[super viewDidLoad];
SSLPerson *person = [[SSLPerson alloc] init];
[person setValue:@"SSL" forKey:@"name"];
NSLog(@"_name = %@",person->_name);
NSLog(@"_isName = %@",person->_isName);
NSLog(@"name = %@",person->name);
NSLog(@"isName = %@",person->isName);
}
查看打印结果:
_name = SSL
_isName = (null)
name = (null)
isName = (null)
- 只有
_name被赋值,没有问题。
测试代码去掉_name:
@interface SSLPerson : NSObject{
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
@implementation SSLPerson
@end
- (void)viewDidLoad {
[super viewDidLoad];
SSLPerson *person = [[SSLPerson alloc] init];
[person setValue:@"SSL" forKey:@"name"];
NSLog(@"_isName = %@",person->_isName);
NSLog(@"name = %@",person->name);
NSLog(@"isName = %@",person->isName);
}
查看打印结果:
_isName = SSL
name = (null)
isName = (null)
- 只有
_isName被赋值,没有问题,剩下的两种情况经过测试也是没问题的。
3. setValue:forUndefinedKey:
调用setValue:forUndefinedKey:方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。
KVC 取值过程
打开 苹果官方文档 查看KVC的取值原理:
Search Pattern for the Basic Getter
The default implementation of
valueForKey:, given akeyparameter as input, carries out the following procedure, operating from within the class instance receiving thevalueForKey:call.
- Search the instance for the first accessor method found with a name like
get<Key>,<key>,is<Key>, or_<key>, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.- If no simple accessor method is found, search the instance for methods whose names match the patterns
countOf<Key>andobjectIn<Key>AtIndex:(corresponding to the primitive methods defined by theNSArrayclass) and<key>AtIndexes:(corresponding to theNSArraymethodobjectsAtIndexes:).
If the first of these and at least one of the other two is found, create a collection proxy object that responds to allNSArraymethods and return that. Otherwise, proceed to step 3.
The proxy object subsequently converts anyNSArraymessages it receives to some combination ofcountOf<Key>,objectIn<Key>AtIndex:, and<key>AtIndexes:messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name likeget<Key>:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were anNSArray, even if it is not.- If no simple accessor method or group of array access methods is found, look for a triple of methods named
countOf<Key>,enumeratorOf<Key>, andmemberOf<Key>:(corresponding to the primitive methods defined by theNSSetclass).
If all three methods are found, create a collection proxy object that responds to allNSSetmethods and return that. Otherwise, proceed to step 4.
This proxy object subsequently converts anyNSSetmessage it receives into some combination ofcountOf<Key>,enumeratorOf<Key>, andmemberOf<Key>:messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were anNSSet, even if it is not.- If no simple accessor method or group of collection access methods is found, and if the receiver's class method
accessInstanceVariablesDirectlyreturnsYES, search for an instance variable named_<key>,_is<Key>,<key>, oris<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.- If the retrieved property value is an object pointer, simply return the result.
If the value is a scalar type supported byNSNumber, store it in anNSNumberinstance and return that.
If the result is a scalar type not supported by NSNumber, convert to anNSValueobject and return that.- If all else fails, invoke
valueForUndefinedKey:. This raises an exception by default, but a subclass ofNSObjectmay provide key-specific behavior.]
1. get<Key>, <key>, is<Key>, or _<key>
按照get<Key>、<key>、is<Key>、_<key>顺序查找方法。
如果找到就调用取值并执行5,否则执行下一步;
2. countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:
查找countOf<Key>、objectIn<Key>AtIndex:、<key>AtIndexes:命名的方法。
如果找到第一个和后面两个中的至少一个,则创建一个能够响应所有NSArray的方法的集合代理对象(类型为NSKeyValueArray,继承自NSArray),并返回该对象。否则执行3;
- 代理对象随后将其接收到的任何
NSArray消息转换为countOf<Key>、objectIn<Key>AtIndex:、<Key>AtIndexes:消息的组合,并将其发送给KVC调用方。如果原始对象还实现了一个名为get<Key>:range:的可选方法,则代理对象也会在适当时使用该方法。 - 当
KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSArray一样,即使它不是NSArray。
3. countOf<Key>、enumeratorOf<Key>、memberOf<Key>:
查找countOf<Key>、enumeratorOf<Key>、memberOf<Key>:命名的方法。
如果三个方法都找到,则创建一个能够响应所有NSSet的方法的集合代理对象(类型为NSKeyValueSet,继承自NSSet),并返回该对象。否则执行4;
- 代理对象随后将其接收到的任何
NSSet消息转换为countOf<Key>、enumeratorOf<Key>、memberOf<Key>:消息的组合,并将其发送给KVC调用方。 - 当
KVC调用方与代理对象一起工作时,允许底层属性的行为如同NSSet一样,即使它不是NSSet。
4. +accessInstanceVariablesDirectly
查看消息接收者类的+accessInstanceVariablesDirectly方法的返回值(默认返回YES)。如果返回YES,就按照_<key>、_is<Key>、<key>、is<Key>顺序查找成员变量。如果找到就直接取值并执行5,否则执行6。如果+accessInstanceVariablesDirectly方法返回NO也执行6。
5. 根据需要进行数据类型转换
如果取到的值是一个对象指针,即获取的是对象,则直接将对象返回。
如果取到的值是一个NSNumber支持的数据类型,则将其存储在NSNumber实例并返回。
如果取到的值不是一个NSNumber支持的数据类型,则转换为NSValue对象, 然后返回。
6. valueForUndefinedKey:
调用valueForUndefinedKey:方法,该方法抛出异常NSUnknownKeyException,并导致程序Crash。这是默认实现,我们可以重写该方法根据特定key做一些特殊处理。
四、 KVC 自定义实现
自定义设值
- (void)ssl_setValue:(nullable id)value forKey:(NSString *)key {
// 1: key 判空
if (key == nil || key.length == 0) {
return;
}
// 2: setter set<Key>: or _set<Key>,
// key 首字母大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
if ([self ssl_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self ssl_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self ssl_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
// 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
// 3: 判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4: 间接变量
// 获取 ivar -> 遍历 containsObjct -
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
// 4.2 获取相应的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 对相应的 ivar 设置值
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
// 5:如果找不到相关实例
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
自定义取值
- (nullable id)ssl_valueForKey:(NSString *)key {
// 1:刷选key 判断非空
if (key == nil || key.length == 0) {
return nil;
}
// 2:找到相关方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex
// key 要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// _name -> _isName -> name -> isName
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
return @"";
}
相关方法
#pragma mark - 相关方法
- (BOOL)ssl_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
- (id)performSelectorWithMethodName:(NSString *)methodName{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
}
return nil;
}
- (NSMutableArray *)getIvarListName{
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++) {
Ivar ivar = ivars[i];
const char *ivarNameChar = ivar_getName(ivar);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@",ivarName);
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}