KVC原理分析

KVC的使用

LGPerson对象有以下几个属性

@property (nonatomic, copy)   NSString          *name;
@property (nonatomic, strong) NSArray           *array;
@property (nonatomic, strong) NSMutableArray    *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic)         ThreeFloats       threeFloats;
@property (nonatomic, strong) LGStudent         *student;
  • 我们可以通过setter方法直接进行赋值。
LGPerson *person = [[LGPerson alloc] init];
// 一般setter 方法
person.name      = @"LG_Cooci";
person.age       = 18;
person->myName   = @"cooci";
NSLog(@"%@ - %d - %@",person.name,person.age,person->myName);
  • 我们也可以通过KVC的方式进行属性的赋值
// 1:Key-Value Coding (KVC) : 基本类型
[person setValue:@"KC" forKey:@"name"];
[person setValue:@19 forKey:@"age"];
[person setValue:@"酷C" forKey:@"myName"];
NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);

person的array属性为不可变array,怎么通过KVC进行赋值呢?

person.array = @[@"1",@"2",@"3"];
// 由于不是可变数组 - 这里直接赋值会报错
person.array[0] = @"100";

由于array为不是可变数组,直接通过person.array[0] = @"100";赋值会报错。我们可以通过下面的方式,将一个新的array赋值给array来实现替换数组的第一个元素。

NSArray *array = [person valueForKey:@"array"];
// 用 array 的值创建一个新的数组
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);

还可以通过mutableArrayValueForKey,获取到一个可变数组,然后更改可变数组的元素,就会相应的更改了array的元素。

NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"100";
NSLog(@"%@",[person valueForKey:@"array"]);

如果对象的属性不是OC对象,怎么通过KVC赋值呢?
已知ThreeFloats为一个struct

typedef struct {
    float x, y, z;
} ThreeFloats;

如果我们直接通过下面的方式进行KVC赋值的话,编译器会报错。

ThreeFloats floats = {1., 2., 3.};
[person setValue:floats forKey:@"threeFloats"];

因为ThreeFloats不是OC对象,所以无法直接通过KVC赋值。但是我们可以通过NSValue来进行包装一下,然后再通过KVC赋值。

ThreeFloats floats = {1., 2., 3.};
NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];

通过KVC取值的时候,也是通过NSValue获取。

NSValue *reslut = [person valueForKey:@"threeFloats"];
NSLog(@"%@",reslut);
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);

如果我们要对对象的属性的属性进行KVC操作的话,可以通过keyPath来访问处理

// 5:KVC - 层层访问
LGStudent *student = [[LGStudent alloc] init];
student.subject    = @"iOS";
person.student     = student;
[person setValue:@"大师班" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

KVC的原理

我们可以在developer.apple.com中的Documentation中查找到KVC的文档介绍:Key-Value Coding Programming Guide
其中的Accessor Search Patterns(访问器搜索模式),对于setter方法的描述:

  1. 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.
  1. If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.
  1. Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior

意思为:1、首先会搜索属性的set<Key>:、set<Key>方法;
2、如果accessInstanceVariablesDirectly方法返回为YES的话,接下来就会去搜索对应的成员变量
<key>, _is<Key>, <key>, or is<Key>,按照这个顺序搜索;
3、如果第一步和第二步都没有搜索到的话,就会调用setValue:forUndefinedKey:方法。
我们可以通过代码验证一下上面的搜索模式:
为LGPerson定义下面的几个属性

@interface LGPerson : NSObject{
    @public
     NSString *_name;
     NSString *_isName;
     NSString *name;
     NSString *isName;
}

实现setName、_setName和setIsName方法

//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);
}

然后我们通过KVC对person的name进行赋值。然后看看会调用哪个set方法呢?

// 1: KVC - 设置值的过程
[person setValue:@"LG_Cooci" forKey:@"name"];

实验结果可知,优先查找setName,如果没有实现setName的话,再去查找_setName。他们的优先顺序为setName > _setName > setIsName。
按照文档上说的。如果没有实现set方法的话,并且accessInstanceVariablesDirectly方法返回为YES,就会去查找实例变量。现在我们把上面的三个set方法全部注释。然后看看访问实例变量的顺序:

NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);

然后我们得出结果,属性的访问顺序为_name > _isName > name > isName。
注意:如果此时我们将accessInstanceVariablesDirectly方法返回为NO,就不会去查找实例变量。如果没有实现set方法的话就会直接抛出异常。
上面我们研究的是通过KVC设置value,下面我们来看下通过KVC获取value。
查看文档"Search Pattern for the Basic Getter"可知:

  • 首先会按照顺序查找下面的方法:get<Key>, <key>, is<Key>, or _<key>;
  • 如果第一步没有找到,判断是否为NSArray,查找countOf<Key>, enumeratorOf<Key>和memberOf<Key>: ;
  • 然后判断是否为NSSet,查找countOf<Key>, enumeratorOf<Key>和memberOf<Key>: ;
  • 如果accessInstanceVariablesDirectly返回为YES,就会顺序查看成员变量:_<key>, _is<Key>, <key>, or is<Key>;
  • 如果找到对应的value,如果是对象指针类型,就直接返回;如果是NSNumber支持的标量类型,将值储存在NSNumber中返回;如果结果不是NSNumber支持的标量类型,则将值转化成NSValue返回。
  • 最后没有找到的话,就会调用valueForUndefinedKey:方法。
    验证方法和上面的获取值得方式相似,此处我们不再验证。

自定义实现KVC

了解了KVC的原理后,我们就可以自己模仿实现一套KVC机制了。
首先为NSObject创建Category,在category中定义KVC方法。

@interface NSObject (LGKVC)

// LG KVC 自定义入口
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)lg_valueForKey:(NSString *)key;
@end

然后实现该方法

一、首先我们来实现set方法:
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
  1. 进行非空判断;
if (key == nil  || key.length == 0) return;
  1. 找到相关方法 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 lg_performSelectorWithMethodName:setKey value:value]) {
    NSLog(@"*********%@**********",setKey);
    return;
}else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
    NSLog(@"*********%@**********",_setKey);
    return;
}else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
    NSLog(@"*********%@**********",setIsKey);
    return;
}

其中lg_performSelectorWithMethodName是我们自己实现的根据字符串调用方法的自定义方法。

  1. 判断是否能够直接赋值实例变量;
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];
}
  1. 找相关实例变量进行赋值
// 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;
}

其中的getIvarListName是我们自己实现的获取成员变量列表的方法。

  1. 如果找不到相关实例,就抛出错误;
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];

这样我们就实现了KVC的set方法,下面我们来实现get方法

二、实现KVC的get方法:
- (nullable id)lg_valueForKey:(NSString *)key
  1. key判断非空;
if (key == nil  || key.length == 0) {
        return nil;
    }
  1. 找到相关方法 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
  1. 判断是否能够直接访问实例变量;
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];
}
  1. 找相关实例变量;
// 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);;
}
  1. 如果上面的步骤都没有找到value,抛出异常;
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];

KVC异常处理小技巧

1. 类型转化

使用KVC的时候,难免会出现异常数据的情况,下面我们来看下怎么容错:
已知person存在下面属性

@property (nonatomic, copy) NSString *subject;
@property (nonatomic, assign) int  age;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic) ThreeFloats  threeFloats;

因为age是int类型,我们使用KVC赋值的时候需要使用NSNumber类型赋值。

[person setValue:@18 forKey:@"age"];

但是想下面赋值,给int类型的赋值string类型,会怎么样呢?有打印结果可知,KVC会自动进行类型转化,将String类型的value转化为NSCFNumber类型。

[person setValue:@"20" forKey:@"age"]; // int - string
NSLog(@"age === %@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber

sex属性类型是Bool类型,给他赋值String类型的@"20",也会自动转为为__NSCFBoolean类型的1。

[person setValue:@"20" forKey:@"sex"];
    NSLog(@"%@-%@",[person valueForKey:@"sex"],[[person valueForKey:@"sex"] class]);//__NSCFNumber

然后看下结构体类型的转化

typedef struct {
    float x, y, z;
} ThreeFloats;
ThreeFloats floats = {1., 2., 3.};
NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue

结构体通过NSValue赋值,KVC会转化为NSConcreteValue类。

2. 赋值nil;

如果我们像下面一样赋值nil,会怎么样呢?

// 2: 设置空值
NSLog(@"******2: 设置空值******");
[person setValue:nil forKey:@"age"]; // subject不会走 - 官方注释里面说只对 NSNumber - NSValue
[person setValue:nil forKey:@"subject"];

setNilValueForKey方法的文档介绍可知,如果是NSNumber 或者NSValue类型赋值nil,会调用setNilValueForKey方法。如果不复写该方法,该方法默认会抛出错误。

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"你傻不傻: 设置 %@ 是空值",key);
}
3. 赋值或者取值时找不到key;
// 3: 找不到的 key
NSLog(@"******3: 找不到的 key******");
[person setValue:nil forKey:@"KC"]; 

// 4: 取值时 - 找不到 key
NSLog(@"******4: 取值时 - 找不到 key******");
NSLog(@"%@",[person valueForKey:@"KC"]);

会调用下面的方法,默认实现也是抛出错误。

- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1.kvc:Key-Value Coding 基本类型使用 集合类型使用 非对象类型,转换成相应的NSValue,...
    尘舒阅读 3,764评论 0 2
  • KVC(Key-valuecoding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS...
    榕樹頭阅读 3,989评论 0 2
  • 1. Basic methods KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允...
    木小易Ying阅读 1,306评论 0 4
  • KVC(Key-value coding)键值编码,iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,...
    CALayer_Sai阅读 7,282评论 0 4
  • 1.KVC定义 KVC ( Key-Value-Coding) 键值编码。指iOS开发中允许用户可以直接通过ke...
    碎梦_aimee阅读 1,553评论 0 0