前言
KVC
的全称是 Key-Value-Coding
,翻译成中文是 键值编码
,键值编码是由NSKeyValueCoding
非正式协议启动的一种机制,对象采用该协议来间接访问其属性
。既可以通过一个 字符串Key来访问某个属性
。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。
KVC相关API
常用方法
主要有以下四种常用的方法:
1 2 3 4 5
| // 通过key来取值 - (nullable id)valueForKey:(NSString *)key;
// 通过key来设值 - (void)setValue:(nullable id)value forKey:(NSString *)key;
|
1 2 3 4 5
| // 通过keyPath来取值 -(nullable id)valueForKeyPath:(NSString *)keyPath;
// 通过keyPath来设值 -(void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
|
1 2 3
| 默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索 是否可以直接访问实例变量 +(BOOL)accessInstanceVariablesDirectly;
|
1 2
| KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。 - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
|
1 2
| 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。 - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
|
1 2
| 如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。 - (nullable id)valueForUndefinedKey:(NSString *)key;
|
1 2
| 和上一个方法一样,但这个方法是设值。 - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
|
1 2
| 如果你在SetValue方法时面给Value传nil,则会调用这个方法 - (void)setNilValueForKey:(NSString *)key;
|
1 2
| 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
|
KVC设值底层原理
- 在日常开发中,针对对象属性的
赋值
,一般有以下两种方式
- 直接通过
setter
方法赋值
- 通过
KVC键值编码
的相关API赋值
1 2 3 4 5
| ZJPerson * person = [[ZJPerson alloc] init]; // 1.一般setter方法 person.name = @"ZJ_哈哈"; // 2.KVC方法 [person setValue:@"ZJ_嘻嘻" forKey:@"name"];
|
下面针对使用最多的 KVC
设值方法:seValue:forKey
,来进行底层原理的探索
- 首先进入
seValue:forKey
的声明,发现是在 Foundation
框架中,而Foundation
框架是不开源的,有以下几种方式可以去探索底层:
- 通过
Hopper
反汇编,查看伪代码
- 通过苹果
官方文档
Github
搜索是否有相关的demo
- 在这里,我们通过 Key-Value Coding Programming Guide 苹果官方文档来研究,针对设值流程,有如下说明:
当调用 seValue:forKey
设值属性 value
时,其底层的执行流程为:
【第一步】 首先查找是否有这三种 setter
方法,按照查找顺序为 set<Key> 、_set<Key>、setIs<Key>
【第二步】 如果没有第一步的三个简单的setter方法,则查找accessInstanceVariablesDirectly
是否返回 YES
【第三步】 如果 setter方法
或者 实例变量
都没有找到,系统会执行该对象的 setValue:forUnderfinedKey:
方法,默认抛出 NSUnderfinedKeyException
类型的异常
- 综上所述,KVC通过
seValue:forKey
方法设值的流程,以ZJPerson的对象person的属性name为例,如下图所示:
KVC取值底层原理
同样的,我们可以通过官方文档分析KVC取值的底层原理
当调用 valueForKey
时,其底层的执行流程如下:
【第一步】 首先查找 getter
方法,按照:get<Key>、<key>、is<Key>、_<key>
的方法顺序查找
- 如果
找到
,则进入 【第五步】
- 如果
没有找到
,则进入 【第二步】
【第二步】 如果第一步中的 getter
方法没有找到,KVC会查找:countOf<Key>、objectIn<Key> AtIndex:、<key>AtIndex:
如果找到 countOf<Key>
和其他两个中的一个,则会创建一个响应所有NSArray
方法的 集合代理对象
,并返回该对象,即 NSKeyValueArray
,是NSArray
的子类。代理对象随后将接收到的所有 NSArray
消息转换为countOf<Key>、objectIn<Key> AtIndex:、<key>AtIndex:
消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为 get<Key>:range:
之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)
如果没有找到这三个访问数组,则继续进入 【第三步】
【第三步】 如果没有找到上面的几种方法,则会同时查找 countOf <Key>,enumeratorOf<Key>和memberOf<Key>
这三个方法
【第四步】 如果还没有找到,检查类方法 InstanceVariablesDirectly
是否 YES
,依次搜索 _<key>,_is<Key>
,<key>
或 is<Key>
的实例变量
【第五步】 根据搜索到的属性值的类型,返回不同的结果
【第六步】 如果上面5步的方法 均失败
,系统会执行该对象的valueForUndefinedKey:
方法,默认抛出 NSUndefinedKeyException
类型的异常
综上所述,KVC通过 valueForKey:
方法取值的流程以设值 ZJPerson
的对象person的属性 name
为例,如下图所示:
自定义KVC
原理:通过给 NSObject
添加 分类ZJKVC
,实现自定义的 zj_setValue:forKey:
和 zj_valueForKey:
方法,根据苹果官方文档提供的查找规则进行实现:
1 2 3 4 5 6
| @interface NSObject (ZJKVC) // 设值 - (void)zj_setValue:(nullable id)value forKey:(NSString *)key; // 取值 - (nullable id)zj_valueForKey:(NSString *)key; @end
|
自定义KVC设值
自定义KVC设值
1、判断 key非空
2、查找 setter
方法,顺序是:setXXX、_setXXX、setIsXXX
3、判断是否响应 accessInstanceVariablesDirectly
方法,即间接访问实例变量
4、间接访问变量赋值(只会走一次),顺序是:_key、_isKey、key、isKey
- 定义一个手机实例变量的可变数组
- 通过
class_getInstanceVariable
方法,获取响应的ivar
- 通过
object_setIvar
方法,对响应的ivar设值值
5、如果找不到相关实例变量,则抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| @implementation NSObject (ZJKVC) // 设值 - (void)zj_setValue:(nullable id)value forKey:(NSString *)key{ // 1.判断key是否存在 if (key == nil || key.length == 0) { return; } // 2.找setter方法,顺序是:setXXX、_setXXX、setIsXXX NSString * Key = key.capitalizedString; // key 要大写 NSString * setKey = [NSString stringWithFormat:@"set%@:", Key]; NSString * _setKey = [NSString stringWithFormat:@"_set%@:", Key]; NSString * setIsKey = [NSString stringWithFormat:@"setIs%@:", Key]; // 判断是否有方法 if ([self zj_performSelectorWithMethodName:setKey value:value]) { NSLog(@"*************%@*************", setKey); return; }else if ([self zj_performSelectorWithMethodName:_setKey value:value]) { NSLog(@"*************%@*************", _setKey); return; }if([self zj_performSelectorWithMethodName:setIsKey value:value]){ NSLog(@"*************%@*************", setIsKey); return; } // 3、判断是否响应accessInstanceVariablesDirectly方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃 if (![self.class accessInstanceVariablesDirectly]) { @throw [NSException exceptionWithName:@"ZJUnKnownKeyException" reason:[NSString stringWithFormat:@"***[%@ valueForUnderfinedKey:] this class is not key value coding-compliant for the key name.***"] userInfo:nil]; } // 4.间接访问变量赋值,顺序为:_key、_isKey、key、isKey // 4.1 定义一个手机实例变量的可变数组 NSMutableArray * mArray = [self getIvarListName]; // _key、_isKey、key、isKey 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:@"ZJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil]; }
|
相关方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #pragma mark - 相关方法 - (BOOL)zj_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; }
|
获取成员变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #pragma mark -获取成员变量名 - (NSMutableArray *)getIvarListName{ // 创建可变数组,用于存储ivar成员变量 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; } @end
|
自定义KVC取值
1、判断 key非空
2、查找相应方法,顺序是 get<Key>、<key>、countOf<Key>、objectIn<Key>AtIndex
3、判断是否能够直接赋值实例变量,即判断是否响应 accessInstanceVariablesDirectly
方法,间接访问实例变量
4、间接访问实例变量,顺序是 _<key>、_is<Key>、<key>、is<Key>
- 定义一个手机实例变量的
可变数组
- 通过
class_getInstanceVariable
方法,获取相应的ivar
- 通过
object_getIvar
方法,返回相应的ivar的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| #pragma mark -取值 - (nullable id)zj_valueForKey:(NSString *)key{ // 1、判断非空 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、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃 if (![self.class accessInstanceVariablesDirectly]) { @throw [NSException exceptionWithName:@"ZJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; } // 4.找相关实例变量进行赋值,顺序为:_<key>、 _is<Key>、 <key>、 is<Key> // 4.1 定义一个收集实例变量的可变数组 NSMutableArray *mArray = [self getIvarListName]; // 例如:_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 @""; }
|
使用路由访问,即keyPath
在日常开发中,一个 类的成员变量
有可能 是自定义类或者其他的复杂数据类型
,一般的操作是,我们可以先通过 KVC
获取该属性,然后再通过 KVC
获取自定义类的属性,就是比较麻烦,还有另一种比较简单的方法,就是使用 KeyPath
即 路由
,涉及一下两个方法: setValue:forKeyPath:
和 valueForKeyPath:
1 2 3 4
| // 通过KeyPath来设值 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通过KeyPath来取值 - (nullable id)valueForKeyPath:(NSString *)kePath;
|
参考如下的案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| // ZJPerson类 @interface ZJPerson : NSObject @property (nonatomic, copy) NSString *age; @property (nonatomic, strong) ZJStudent *student; @end
// ZJStudent类 @interface ZJStudent : NSObject @property (nonatomic, copy) NSString *name; @end
int main(int argc, const char * argv[]) { @autoreleasepool { ZJPerson *person = [[ZJPerson alloc] init]; ZJStudent *student = [ZJStudent alloc]; student.name = @"ZJ"; person.student = student; NSLog(@"改变前:%@",[person valueForKeyPath:@"student.name"]); // 根据kvc - keyPath路由修改student的subject属性的值 [person setValue:@"嘻嘻" forKeyPath:@"student.name"]; NSLog(@"改变后:%@",[person valueForKeyPath:@"student.name"]); } return 0; }
//*************打印结果************* 2021-5-27 09:55:08.512833+0800 001-KVC简介[58089:6301894] 改变前:ZJ 2021-5-27 09:55:08.512929+0800 001-KVC简介[58089:6301894] 改变后:嘻嘻
|
KVC的使用场景
1、动态设值和取值
2、通过KVC访问和修改私有变量
在日常开发中,对于类的私有属性,在外部定义的对象,是 无法直接访问私有属性的
,但是对于KVC而言,一个对象没有自己的隐私,所以可以 通过KVC修改和访问任何私有属性
model和字典的转换可以通过下面两个KVC的API实现
1 2 3 4 5 6
| // 字典转模型 - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
// 模型转字典 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
|
在日常开发中,我们知道,很多UI控件都是在其内部由多个UI空间组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,常用的就是 自定义tabbar、个性化UITextField中的placeHolderText
在对容器类使用KVC时,valueForKey:
将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回
另一个集合
如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| // KVC实现高阶消息传递 - (void)transmitMsg{ NSArray *arrStr = @[@"english", @"franch", @"chinese"]; NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"]; for (NSString *str in arrCapStr) { NSLog(@"%@", str); } NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"]; for (NSNumber *length in arrCapStrLength) { NSLog(@"%ld", (long)length.integerValue); } }
//********打印结果******** 2021-5-27 11:33:43.377672+0800 CJLCustom[60035:6380757] English 2021-5-27 11:33:43.377773+0800 CJLCustom[60035:6380757] Franch 2021-5-27 11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese 2021-5-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7 2021-5-27 11:33:43.378327+0800 CJLCustom[60035:6380757] 6 2021-5-27 11:33:43.378417+0800 CJLCustom[60035:6380757] 7
|