美文网首页
KVC原理分析

KVC原理分析

作者: 半边枫叶 | 来源:发表于2020-02-15 20:22 被阅读0次

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;

相关文章

  • iOS底层学习文章

    iOS黑魔法-Method Swizzling Objective-C 反射机制 KVC原理剖析 KVO原理分析及...

  • KVC原理分析

    KVC的使用 LGPerson对象有以下几个属性 我们可以通过setter方法直接进行赋值。 我们也可以通过KVC...

  • KVC原理分析

    一、KVC简介   KVC(Key-Value Coding)键值编码,是利用NSKeyValueCoding 非...

  • KVC原理分析

    iOS中的KVC,我们都熟记于心了,它的用法一般就是 setValue这样的用法 一、KVC之用法 我们在代码里...

  • KVC原理分析

    KVC底层实现的是setter和getter方法。 KVC简介 代码准备苹果官方文档查阅[https://deve...

  • iOS-KVC相关

    KVC相关 一、 iOS成员变量,实例变量,属性变量的区别 二、KVC取值、赋值原理 *学习方式:1、分析源码 -...

  • iOS原理篇(二): KVC实现原理

    KVC实现原理 什么是 KVC KVC基本使用 KVC 原理 总结 一 、 什么是KVC KVC的全称是Key-V...

  • KVO和KVC的使用及原理解析

    一 KVO基本使用 二 KVO本质原理讲解及代码验证 三 KVC基本使用 四 KVC设值原理 五 KVC取值原理 ...

  • KVC

    一、KVC的原理(赋值取值过程) KVC相关常用的API KVC设置值的原理(setValue: forKey:的...

  • iOS:KVC原理分析

    目录一,基本知识二,setValue:forKey:底层原理三,valueForKey:底层原理四,触发KVO五,...

网友评论

      本文标题:KVC原理分析

      本文链接:https://www.haomeiwen.com/subject/bkhffhtx.html