前言
KVC(Key-Value Coding)键值编码:是一种由NSKeyValueCoding非正式协议启用的机制,对象采用该机制来提供对其属性的间接访问。
KVC是一个基本概念,它是许多其他 Cocoa 技术的基础,例如键值观察、Cocoa 绑定、核心数据和 AppleScript 能力。在某些情况下,键值编码还有助于简化您的代码。
官方文档:Key-Value Coding Programming Guide
通过官方文档你可以完全了解到什么是KVC并且如何去使用。
KVC基础
获取属性值
获取值的通用方法(valueForKey:
) 还有两个相关方法 valueForKeyPath:
和 dictionaryWithValuesForKeys:
;分别返回对应的数据结构。
按照流程:比如这里需要搜索一个[person valueForKey:@"name"];
1: 先按照下面的格式
getName
,name
,isName
,_name
去搜索结果。如果找到
2: 如果没找到,那么执行 NSArray 的相关方法countOfName
和objectInNameAtIndex:
(对应于NSArray类定义的原始方法)和nameAtIndexes:
(对应于NSArray
方法objectsAtIndexes:
)。
3: 如果还是没有找到,寻找NSSet类的方法,方法名为countOfName
,enumeratorOfName
和memberOfName:
(对应于由所定义的原始的方法NSSet类)。
4: 如果发现收集的访问方法没有简单的存取方法或者组,如果接收器的类方法accessInstanceVariablesDirectly
返回YES
,搜索名为实例变量_<key>
,_is<Key>
,<key>
,或者is<Key>
,按照这个顺序。如果找到,直接获取实例变量的值并进行步骤5,否则进行步骤6。
5: 如果检索到的属性值是一个对象指针,只需返回结果即可;如果该值是 支持的变量类型NSNumber
,则将其存储在一个NSNumber
实例中并返回该实例;如果结果是NSNumber
不支持的变量类型,则转换为NSValue
对象并返回
6: 如果所有其他方法都失败,请调用valueForUndefinedKey:
,默认情况下,这会引发异常,但NSObject
的子类可能会提供特定于键的行为。
1、直接搜索格式如下代码所示
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
LGPerson *person = [[LGPerson alloc] init];
//1、获取值
id v = [person valueForKey:@"name"];
NSLog(@"name = '%@'", v);
2、NSArray 集合类的方法
// 个数
- (NSUInteger)countOfPens{
NSLog(@"%s",__func__);
return [self.arr count];
}
// 获取值
- (id)objectInPensAtIndex:(NSUInteger)index {
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"pens %lu", index];
}
// 或者这个
- (id)pensAtIndexes:(NSIndexSet *)indexs {
NSLog(@"%s",__func__);
return [self.arr objectsAtIndexes:indexs];
}
LGPerson *person = [[LGPerson alloc] init];
// 3: KVC - 集合类型
person.arr = @[@"pen0", @"pen1", @"pen2", @"pen3"];
NSArray *array = [person valueForKey:@"pens"];
NSLog(@"%@",[array objectAtIndex:1]);
NSLog(@"%@",[array objectsAtIndexes:[NSIndexSet indexSetWithIndex:2]]);
NSLog(@"%d",[array containsObject:@"pen1"]);
3、NSSet 集合类型的方法
// 个数
- (NSUInteger)countOfBooks{
NSLog(@"%s",__func__);
return [self.set count];
}
// 是否包含这个成员对象
- (id)memberOfBooks:(id)object {
NSLog(@"%s",__func__);
return [self.set containsObject:object] ? object : nil;
}
// 迭代器
- (id)enumeratorOfBooks {
// objectEnumerator
NSLog(@"来了 迭代编译");
return [self.arr reverseObjectEnumerator];
}
person.set = [NSSet setWithArray:person.arr];
NSSet *set = [person valueForKey:@"books"];
[set enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"set遍历 %@",obj);
}];
4、+ accessInstanceVariablesDirectly
方法返回YES。
@interface LGPerson : NSObject{
@public
NSString *_name; //1
NSString *_isName; //2
NSString *name; //3
NSString *isName; //4
}
LGPerson *person = [[LGPerson alloc] init];
person->_isName = @"CD_isName";
person->name = @"CDname";
person->isName = @"CDisName";
person->_name = @"CD_name";
//1、获取值
id v = [person valueForKey:@"name"];
NSLog(@"name = '%@'", v);
5、如果是基本数据类型必须包装成number
,如果结构体就转换成 NSValue
对象。
这也符合面向对象的概念,需要转换为对象。
///基本数据类型
@property (nonatomic, assign) NSInteger mun;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic, copy) NSNumber *hei;
///结构体
@property (nonatomic, assign) CGPoint point;
person.mun = 1234;
person.sex = true;
person.hei = @(188.8);
person.point = CGPointMake(10, 20);
// id num = [person valueForKey:@"num"];
id sex = [person valueForKey:@"sex"];
id hei = [person valueForKey:@"hei"];
id point = [person valueForKey:@"point"];
// NSLog(@"num = %@", num);
NSLog(@"sex = %@", sex);
NSLog(@"hei = %@", hei);
NSLog(@"point = %@", point);
6、如果所有其他方法都失败,请调用 valueForUndefinedKey:
默认情况下,这会引发异常。当然可以重写 valueForUndefinedKey:
实现。
- (id)valueForUndefinedKey:(NSString *)key {
//重写了方法,就可以返回默认值。如果这里设置了,那么上面的 num 就可以获到值了。
return @(-1);
}
设置属性值
同样 setter
也有一些规则。设置值的默认实现方法为 setValue: forKey:
和扩展 setValue: forKeyPath:
这两个方法。
还是以name
为例说明,其中<Key> == Name, <key> = name
1 .按顺序 查找名为
set<Key>:
,_set<Key>:
,setIs<Key>
的方法。
2 .如果查找失败,且accessInstanceVariablesDirectly
返回 true。那么去找实例变量如下:_<key>
,_is<Key>
,<key>
,is<Key>
。
3 .如果依然找不到,那么执行setValue:forUndefinedKey:
。默认实现爆出异常。
1、按照上诉方法去寻找对应的setter
。在测试两个方法 setIs<Key>:
和 _setIs<Key>:
这两个方法。
//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
[person setValue:@"6666" forKey:@"name"];
通过上面的测试。记过发现 setIsName:
执行了,但是 _setIsName:
没有执行。所以实现了三个方法 set<Key>:
,_set<Key>:
和 setIs<Key>:
2 如果 accessInstanceVariablesDirectly
返回 true 。那么执行成员变量查找。如下代码调试。
@interface LGPerson : NSObject{
@public
NSString *_name; //1
NSString *_isName; //2
NSString *name; //3
NSString *isName; //4
}
//按照如下代码分别调试
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
// NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
// NSLog(@"%@-%@",person->name,person->isName);
// NSLog(@"%@",person->isName);
通过上面的代码调试发现,会依次对成员变量 _name
; _isName
; name
; isName
; 赋值。
3 如果上面的步骤都失败,那么会查看是否实现了 -setValue: forUndefinedKey:
否则报异常。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"%s", __func__);
}
使用和自定义KVC
上面既然有了 setValue: forKey:
和 valueForKey:
的流程,那么我们来尝试一下自定义KVC。
头文件, 因为 NSKeyValueCoding
也是分类,所以呀参考分类来写。
@interface NSObject (CDKVC)
///这是两个默认kvc 方法
- (void)cd_setValue:(id)value forKey:(NSString *)key;
- (id)cd_valueForKey:(NSString *)key;
/// 这个方法默认返回true
+ (BOOL)cd_accessInstanceVariablesDirectly;
/// 这两个方法默认实现抛出异常
- (void)cd_setValue:(id)value forUndefinedKey:(NSString *)key;
- (id)cd_valueForUndefinedKey:(NSString *)key;
@end
实现文件
- (void)cd_setValue:(id)value forKey:(NSString *)key {
// KVC 自定义
// 1: 判断什么 key,必须是string,且长度大于0
if (![key isKindOfClass:[NSString class]]) {
return;
}
if (key == nil || key.length == 0) {
return;
}
// 2: 查找 set<Key>: ,_set<Key>: 和 setIs<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 cd_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self cd_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self cd_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
// 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
// 3:判断是否能够直接赋值实例变量
if (![self.class cd_accessInstanceVariablesDirectly] ) {
//3.2 调用默认的 setValue: forUndefinedKey: 这个方法
[self cd_setValue:value forUndefinedKey:key];
}
// 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:如果找不到相关实例
[self cd_setValue:value forUndefinedKey:key];
}
- (id)cd_valueForKey:(NSString *)key {
// 1:刷选key 判断非空
if (![key isKindOfClass:[NSString class]]) {
return nil;
}
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] ) {
// 3.2 判断是否实现了 valueForUndefinedKey:
return [self cd_valueForUndefinedKey:key];
}
// 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);;
}
// 5 最后也没有查找到。
return [self cd_valueForUndefinedKey:key];
}
+ (BOOL)cd_accessInstanceVariablesDirectly {
return true;
}
- (void)cd_setValue:(id)value forUndefinedKey:(NSString *)key {
#if DEBUG
NSLog(@"CDKVC - %s", __func__);
#endif
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
- (id)cd_valueForUndefinedKey:(NSString *)key {
#if DEBUG
NSLog(@"CDKVC - %s", __func__);
#endif
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
#pragma mark - 相关方法
- (BOOL)cd_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];
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}
使用
CDPerson *person = [[CDPerson alloc] init];
[person cd_setValue:@"6666" forKey:@"name"];
应用场景
KVC 的使用场景还是与很多地方的,下面是笔者用到的一些地方。当然还有其他的地方。
1 通过KVC 可以修改一些系统类的内部函数。
2 通过KVC 可以实现字典模型转换的自动实现。
3 一些简单数据结构,没必要采用模型化,直接KVC赋、取值。
4 系统的一些功能,比如相机拍照的参数,音视频的编解码参数等。