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;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容

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