KVC
的全称是Key-Value Coding
,翻译成中文是 键值编码
,键值编码是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用该协议来间接访问其属性
。既可以通过一个字符串key来访问某个属性
。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。
KVC 相关API
常用方法
主要有以下四个常用的方法
1 2 3 4 5
| - (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
|
1 2 3 4 5
| - (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
|
其他方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| + (BOOL)accessInstanceVariablesDirectly;
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
|
KVC 设值 底层原理
在日常开发中,针对对象属性的赋值,一般有以下两种方式
- 直接通过
setter
方法赋值
- 通过KVC键值编码的相关API赋值
1 2 3 4 5
| LGPerson *person = [[LGPerson alloc] init];
person.name = @"CJL_哈哈";
[person setValue:@"CJL_嘻嘻" forKey:@"name"];
|
下面针对使用最多的KVC设值方法:setValue:forKey
,来进行其底层原理的探索。
首先进入setValue:forKey
的声明,发现是在Foundation
框架中,而Foundation框架是不开源的,有以下几种方式可以去探索底层
- 通过
Hopper
反汇编,查看伪代码
- 通过苹果
官方文档
Github
搜索是否有相关的demo
在这里,我们通过Key-Value Coding Programming Guide苹果官方文档来研究,针对设值流程,有如下说明
当调用setValue:forKey:
设置属性value
时,其底层的执行流程为
【第一步】首先查找是否有这三种setter
方法,按照查找顺序为set<Key>:-> _set<Key> -> setIs<Key>
- 如果
有其中任意一个
setter方法,则直接设置属性的value
(主注意:key是指成员变量名
,首字符大小写需要符合KVC的命名规范)
- 如果都
没有
,则进入【第二步】
【第二步】:如果没有第一步中的三个简单的setter方法,则查找accessInstanceVariablesDirectly
是否返回YES
,
如果返回
,则查找间接访问的实例变量进行赋值,查找顺序为:
1
| _<key> -> _is<Key> -> <key> -> is<Key>
|
- 如果找到其中任意一个实例变量,则赋值
- 如果都没有,则进入【第三步】
如果返回NO
,则进入【第三步】
【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:
方法,默认抛出NSUndefinedKeyException
类型的异常
综上所述,KVC通过 setValue:forKey: 方法设值
的流程以设置LGPerson的对象person的属性name为例,如下图所示
KVC 取值 底层原理
同样的,我们可以通过官方文档分析KVC取值的底层原理
当调用valueForKey:
时,其底层的执行流程如下
【第一步】首先查找getter方法,按照
1
| get<Key> -> <key> -> is<Key> -> _<key>
|
的方法顺序查找,
- 如果
找到
,则进入【第五步】
- 如果
没有找到
,则进入【第二步】
【第二步】如果第一步中的getter方法没有找到,KVC会查找
1
| countOf <Key>和objectIn <Key> AtIndex :和<key> AtIndexes :
|
- 如果找到
countOf <Key>
和其他两个中的一个,则会创建一个响应所有NSArray
方法的集合代理对象
,并返回该对象,即NSKeyValueArray
,是NSArray
的子类
。代理对象随后将接收到的所有NSArray消息转换为countOf<Key>,objectIn<Key> AtIndex:和<key>AtIndexes:
消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get<Key>:range:
之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)
- 如果没有找到这三个访问数组的,请继续进入【第三步】
【第三步】如果没有找到上面的几种方法,则会同时查找
1
| countOf <Key>,enumeratorOf<Key>和memberOf<Key>
|
这三个方法
- 如果这三个方法都找到,则会创建一个响应
所有NSSet方法的集合代理对象
,并返回该对象,此代理对象随后将其收到的所有NSSet
消息转换为countOf<Key>,enumeratorOf<Key>和memberOf<Key>:
消息的某种组合,用于创建它的对象
- 如果还是没有找到,则进入【第四步】
【第四步】如果还没有找到,检查类方法
1
| InstanceVariablesDirectly
|
是否
,依次搜索
1
| _<key>,_is<Key>,<key>或is<Key>
|
的实例变量
【第五步】根据搜索到的
,返回不同的结果
- 如果是
对象指针
,则直接返回结果
- 如果是
NSNumber支持
的标量类型,则将其存储在NSNumber实例
中并返回它
- 如果是是
NSNumber不支持
的标量类型,请转换为NSValue对象
并返回该对象
【第六步】如果上面5步的方法均失败
,系统会执行该对象的valueForUndefinedKey:
方法,默认抛出NSUndefinedKeyException
类型的异常
综上所述,KVC通过 valueForKey: 方法取值
的流程以设置LGPerson
的对象person的属性name
为例,如下图所示
自定义KVC
原理:通过给NSObject
添加分类CJLKVC
,实现自定义的cjl_setValue:forKey:
和cjl_valueForKey:
方法,根据苹果官方文档提供的查找规则进行实现
1 2 3 4 5 6 7 8
| @interface NSObject (CJLKVC)
- (void)cjl_setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)cjl_valueForKey:(NSString *)key;
@end
|
自定义KVC设值
自定义KVC设置流程,主要分为以下几个步骤:
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
| - (void)cjl_setValue:(nullable id)value forKey:(NSString *)key{
if (key == nil || key.length == 0) return;
NSString *Key = key.capitalizedString; NSString *setKey = [NSString stringWithFormat:@"set%@:", Key]; NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key]; NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key]; if ([self cjl_performSelectorWithMethodName:setKey value:value]) { NSLog(@"*************%@*************", setKey); return; }else if([self cjl_performSelectorWithMethodName:_setKey value:value]){ NSLog(@"*************%@*************", _setKey); return; }else if([self cjl_performSelectorWithMethodName:setIsKey value:value]){ NSLog(@"*************%@*************", setIsKey); return; }
if (![self.class accessInstanceVariablesDirectly]) { @throw [NSException exceptionWithName:@"CJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; }
NSMutableArray *mArray = [self getIvarListName]; 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); 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; }
@throw [NSException exceptionWithName:@"CJLUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil]; }
|
自定义KVC取值
取值的自定义代码如下,分为以下几步
- 1、判断
key非空
- 2、查找相应方法,顺序是:
get<Key>、 <key>、 countOf<Key>、 objectIn<Key>AtIndex
- 3、判断是否能够直接赋值实例变量,即判断是否响应
accessInstanceVariablesDirectly
方法,间接访问实例变量,
- 4、间接访问实例变量,顺序是
:_<key> _is<Key> <key> is<Key>
- 4.1 定义一个收集实例变量的
可变数组
- 4.2 通过
class_getInstanceVariable
方法,获取相应的 ivar
- 4.3 通过
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 71
| - (nullable id)cjl_valueForKey:(NSString *)key{
if (key == nil || key.length == 0) { return nil; }
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
if (![self.class accessInstanceVariablesDirectly]) { @throw [NSException exceptionWithName:@"CJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; }
NSMutableArray *mArray = [self getIvarListName]; 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 @""; return @""; }
|
使用路由访问,即keyPath
在日常开发中,一个类的成员变量有可能是自定义类或者其他的复杂数据类型,一般的操作是,我们可以先通过KVC获取该属性,然后再通过KVC获取自定义类的属性,就是比较麻烦,还有另一种比较简便的方法,就是使用KeyPath
即路由
,涉及以下两个方法:setValue:forKeyPath:
和 valueForKeyPath:
1 2 3 4 5
| - (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
|
参考如下的案例
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
| @interface CJLPerson : NSObject @property (nonatomic, copy) NSString *age; @property (nonatomic, strong) CJLStudent *student; @end
@interface CJLStudent : NSObject @property (nonatomic, copy) NSString *name; @end
int main(int argc, const char * argv[]) { @autoreleasepool { CJLPerson *person = [[CJLPerson alloc] init]; CJLStudent *student = [CJLStudent alloc]; student.name = @"CJL"; person.student = student; [person setValue:@"嘻嘻" forKeyPath:@"student.name"]; NSLog(@"%@",[person valueForKeyPath:@"student.name"]); } return 0; }
2020-10-27 09:55:08.512833+0800 001-KVC简介[58089:6301894] 改变前:CJL 2020-10-27 09:55:08.512929+0800 001-KVC简介[58089:6301894] 改变后:嘻嘻
|
KVC 使用场景
1、动态设值和取值
- 常用的可以通过
setValue:forKey:
和 valueForKey:
- 也可以通过
路由
的方式setValue:forKeyPath:
和 valueForKeyPath:
2、通过KVC访问和修改私有变量
在日常开发中,对于类的私有属性
,在外部定义的对象,是无法直接访问私有属性的,但是对于KVC而言,一个对象没有自己的隐私
,所以可以通过KVC修改和访问任何私有属性
3、多值操作(model和字典互转)
model和字典的转换可以通过下面两个KVC的API实现
1 2 3 4 5
| - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
|
4、修改一些系统空间的内部属性
在日常开发中,我们知道,很多UI控件都是在其内部由多个UI空间组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,常用的就是自定义tabbar
、个性化UITextField
中的placeHolderText
5、用KVC实现高阶消息传递
在对容器类使用KVC
时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回 另一个集合
如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| - (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); } }
2020-10-27 11:33:43.377672+0800 CJLCustom[60035:6380757] English 2020-10-27 11:33:43.377773+0800 CJLCustom[60035:6380757] Franch 2020-10-27 11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese 2020-10-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7 2020-10-27 11:33:43.378327+0800 CJLCustom[60035:6380757] 6 2020-10-27 11:33:43.378417+0800 CJLCustom[60035:6380757] 7
|