KVC
全称Key-Value Coding
,俗称键值编码
,是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用该协议可以间接访问其属性
,可以通过一个字符串Key
来访问某个属性
。这种间接访问机制补充了是咧变量及其相关的访问器方法所提供的直接访问。
相关API
- 通过Key取值和设值
//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;
//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- 通过
keyPath(路由)
取值和设值
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- 其他方法
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC底层原理
通过setValue:forKey
方法声明,我们发现是在Foundation
框架中,由于Foundation
是不开源的,我们可以通过下面几种方式来探索
- 通过
Hopper
反汇编,查看伪代码 -
Github
搜索相关文档 - 通过苹果官方文档
下面我们通过官方文档来查看KVC的底层原理
KVC设值流程
-
【第一步】查找三种
setter
方法,查找顺序是set<Key> --> _set<Key> --> setIs<Key>
- 如果
包含其中任意一个
,直接设值属性value(key是指成员变量名
,首字符大小写需要符合KVC的命名规范
) - 如果没有,进入【第二步】
- 如果
-
【第二步】判断
accessInstanceVariablesDirectly
是否返回YES
- 如果返回YES,则查找实例变量进行赋值,查找顺序是
_<Key> --> _is<Key> --> <Key> -- > is<Key>
- 如果找到任意一个就进行赋值
- 如果没有找到进入【第三步】
- 如果返回YES,则查找实例变量进行赋值,查找顺序是
-
【第三步】如果
setter方法
和实例变量
都没找到,会执行setValue:forUndefinedKey:
,默认抛出NSUndefinedKeyException
异常
KVC取值流程
-
【第一步】查找
getter方法
,查找顺序是get<Key> --> <Key> --> is<Key> --> _<Key>
- 找到,执行【第五步】
- 没有找到,执行【第二步】
-
【第二步】在实例中搜索
countOf<Key>
、objectIn<Key>AtIndex:
、objectsAtIndexes :
如果找到
countOf<Key>
和其余任意一个,创建一个响应所有NSArray方法
的集合代理
对象并返回,然后代理对象将收到的所有NSArray消息转化成countOf<Key>,objectIn<Key>AtIndex:和<key>AtIndexes:
消息的组合,用来创建键值编码对象,如果原始对象还实现了get<Key>:range:
之类的可选方法,则代理对象也将在适当的时候使用该方法,实际上,代理对象与与键值编码兼容的对象一起使用,可以使基础属性的行为就好像它是NSArray,即使不是。(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)没找到,执行【第三步】
-
【第三步】同时查找
countOf <Key>,enumeratorOf<Key>和memberOf<Key>
这三个方法- 如果三个方法都找到,创建一个响应所有
NSSet
方法的集合代理对象并将其返回 - 没有找到,执行【第四步】
- 如果三个方法都找到,创建一个响应所有
-
【第四步】检测类方法
InstanceVariablesDirectly
是否为YES
,依次搜索_<key>,_is<Key>,<key>或is<Key>
的实例变量- 如果找到,
直接获取实例变量的值
,执行【第五步】 - 没有找到,执行【第六步】
- 如果找到,
-
【第六步】执行对象方法
valueForUndefinedKey :
,默认抛出NSUndefinedKeyException
的异常
KVC使用场景
1、动态设值和取值
- 通过
setValue:forKey:
和valueForKey:
- 通过
路由
的方式setValue:forKeyPath:
和valueForKeyPath:
2、访问和修改私有变量
对于类的私有属性
,在外部定义的对象,是无法直接访问私有属性的,但是对于KVC而言,一个对象没有自己的隐私,可以通过KVC修改和访问任何私有属性
3、多值操作(model和字典互转)
model和字典的转换可以通过下面两个KVC的API实现
//字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
//模型转字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
4、修改一些系统的内部属性
很多UI控件并没有提供访问的API,但是使用KVC可以解决这个问题,自定义tabbar
、个性化UITextField中
的placeHolderText
5、高阶消息传递
在对容器类使用KVC
时,valueForKey:将会被传递给容器中的每一个对象
,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回 另一个集合
//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);
}
}
//********打印结果********
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
自定义KVC
自定义KVC设值
- 1、判断
key是否为空
- 2、查找
setter
方法,set<Key>、_set<Key>、 setIs<Key>
- 3、判断是否能间接访问实例变量,即是否响应
accessInstanceVariablesDirectly
方法,- YES 继续下一步
- NO 崩溃报错
- 4、间接访问变量赋值(只会走一次)
_key、_isKey、key、isKey
- 4.1、定义一个收集实例变量的集合
- 4.2、
class_getInstanceVariable
获取对应的ivar(实例变量) - 4.3、
object_setIvar
,设置对应的ivar(实例变量)值
- 5、如果找不到相关实例变量,则抛出异常
自定义KVC取值
- 1、判断
Key空值
- 2、查找对应的方法
get<kKey>、<Key>、countof<Key>、objectIn<Key>AtIndex
- 3、判断是否能间接赋值实例变量,即判断是否响应
accessInstanceVariablesDirectly
方法- YES 继续下一步
- NO 崩溃报错
- 4、间接访问实例变量,
_<key> _is<Key> <key> is<Key>
- 4.1 定义一个收集实例变量的可变数组
- 4.2
class_getInstanceVariable
,获取相应的 ivar - 4.3
object_getIvar
,返回相应的 ivar 的值
keyPath -- 路由访问
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
//CJLPerson类
@interface CJLPerson : NSObject
@property (nonatomic, copy) NSString *age;
@property (nonatomic, strong) CJLStudent *student;
@end
//CJLStudent类
@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;
//根据kvc - keyPath路由修改student的subject属性的值
[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代码
#import "NSObject+YPKVC.h"
#import <objc/runtime.h>
@implementation NSObject (YPKVC)
//设值
- (void)yp_setValue:(nullable id)value forKey:(NSString *)key{
// 1、判断key 是否存在
if (key == nil || key.length == 0) return;
// 2、找setter方法,顺序是:setXXX、_setXXX、 setIsXXX
// key 要大写
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 yp_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*************%@*************", setKey);
return;
}else if([self yp_performSelectorWithMethodName:_setKey value:value]){
NSLog(@"*************%@*************", _setKey);
return;
}else if([self yp_performSelectorWithMethodName:setIsKey value:value]){
NSLog(@"*************%@*************", setIsKey);
return;
}
// 3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
if (![self.class accessInstanceVariablesDirectly]) {
@throw [NSException exceptionWithName:@"YPUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4、间接访问变量赋值,顺序为:_key、_isKey、key、isKey
// 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:@"YPUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
//取值
- (nullable id)yp_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:@"YPUnKnownKeyException" 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 @"";
return @"";
}
#pragma mark - 相关方法
- (BOOL)yp_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{
//创建可变数组,用于存储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