写在前面
之前写事岀无常必有妖-iOS捉妖记之(Runtime)时说好要写一篇关于runtime的详细介绍的。看到这个标题关注了我的小伙伴们放下手里的西瓜刀,小的并没有弃坑,只是有简友评论希望多结合实例来介绍runtime所以这几天正苦苦搜寻通俗易懂又比较有价值的实例,所以先来水一篇KVC相关的文章。
文章最后会鸡贼的教大家一个KVC实用技巧,打造一个万能容器对象!如果你们公司的后台返回的参数飘忽不定而老项目中接收后台返回数据的又是用的一个通用模型时就可以派上用场了!
看完本篇之后你将获得:
- 了解什么是kvc
- kvc特性
- 掌握用kvc实现一个万能容器对象的方法
- 归纳kvc的优缺点
引言
kvc是每个iOS开发者在学习obj-C时都学过的特性,但是由于obj-C学起来并不难,所以很多初学者把大部分时间都放在熟悉cocoa框架以及对iOS开发相关的API掌握上。其实在项目中巧用kvc可以大大提高开发效率减少代码量,下面我们进入正题。
定义
KVC(Key-Value-Coding)意思是键值编码。在iOS中,提供了一种方法通过使用属性的名称(也就是Key)来间接访问对象的属性方法。
说的有点儿拗口,实际上就是通过类定义我们可以看到类的各种属性,那么使用属性的名称我们就能访问到类实例化后的对象的这个属性值。
KVC特性
1.无setter/getter方法也可以直接去找对应名称的变量操作
KVC中最常见的也是最基本的调用方法:
//调用这个方法找到与key匹配的变量并把value赋值给它
- (void)setValue:(nullable id)value forKey:(NSString *)key;
//调用这个方法找到与key匹配的变量并返回
- (nullable id)valueForKey:(NSString *)key;
那么上面的方法对比直接调用getter/setter方法时有什么区别呢?
- 调用上面的方法其实默认会调用接收消息对象的getter/setter方法来对与key键匹配的属性进行读/写操作
- 如果接受消息的对象并没有实现相应的getter/setter方法的话,会直接访问对象中的匹配变量作相应操作(包括私有变量)
第一点印证实例:
@interface LXObj : NSObject
{
NSString *ivar;
}
@end
在.m文件中的代码空空如也:
@implementation LXObj
@end
然后执行以下代码:
LXObj *obj = [[LXObj alloc] init];
obj.ivar = @"i am a ivar";
发现静态解析器编译不过去……
如果按照以下代码去操作ivar变量即可使程序正常运行:
LXObj *obj = [[LXObj alloc] init];
[obj setValue:@"i am a ivar" forKey:@"ivar"];
NSLog(@"%@", [obj valueForKey:@"ivar"]);
运行结果如下:

KVC可以正常运行.png
那么我们再来看一下KVC访问下的所谓obj-C私有变量,首先在类中添加私有变量:
@interface LXObj : NSObject {
@private NSString *privateIvar;
}
或者在类扩展中声明变量:
@interface LXObj ()
@property (nonatomic, copy) NSString *privateIvar;
@end
为了解释的更明白,让所有人(包括一些初学obj-C的小伙伴)看懂,先用普通的方式访问:

普通方式访问私有变量.png
都编译不过去,静态分析器就帮我们找到了错误,那么我们换KVC方式访问:
LXObj *obj = [[LXObj alloc] init];
[obj setValue:@"i am a private ivar" forKey:@"privateIvar"];
NSLog(@"%@", [obj valueForKey:@"privateIvar"]);
然后运行,发现可以正常运行:

KVC方式访问私有变量.png
看到这,你会得出或者印证你以前学习obj-C时书上提到的:obj-C实际上并不存在真正的私有变量,因为只要知道变量名称就可以访问且操作这个变量。
你不要萌萌的瞪着眼睛问我:我们为啥还要遵守规则把变量写在.m内的类扩展中呢?
因为当你的app提交到appstore中被人下载得到下载包后,别人反编译分分钟就能看到你的项目头文件,类名和方法(其实我不会告诉你.m文件也能看到,只不过反编译后的.m依旧是一坨坨的代码不像.h那样容易看懂。不用担心你还可以混淆啊,你不要问我obj-C怎么混淆,再说下去就跑题了……额,好吧我承认对obj-C混淆也不太懂>_<|||)。< p="">
2.使用KVC会自动开/封箱
如果你想设置一个标准量,在调用- (void)setValue:(nullable id)value forKey:(NSString *)key方法之前需要将它们封箱:
先给LXObj类添加一个floatNum属性:
@property (nonatomic, assign) float floatNum;
然后执行下面代码:
LXObj *obj = [[LXObj alloc] init];
[obj setValue:[NSNumber numberWithFloat:0.1] forKey:@"floatNum"];
这时,- (void)setValue:(nullable id)value forKey:(NSString *)key方法会先开箱取出该值,再调用- (void)setFloatNum:方法或者直接更改floatNum实例变量,反之:
LXObj *obj = [[LXObj alloc] init];
NSLog(@"%@", [obj valueForKey:@"floatNum"]);
代码中[obj valueForKey:@"floatNum"]方法会先取出floatNum属性的值并封箱打印出来。
3.键路径
当类中包含其他类类型的属性时,可以直接使用键路径来操作这个属性内部的变量。
先定义一个LXSubObj类,其中包含一个属性subIvar:
@interface LXSubObj : NSObject
@property (nonatomic, copy) NSString *subIvar;
@end
然后往LXObj中添加一个LXSubObj类型的属性:
@class LXSubObj;
@interface LXObj : NSObject
@property (nonatomic, strong) LXSubObj *subObj;
@end
此时要操作LXObj对象中的subObj属性的subIvar可以使用
LXObj *obj = [[LXObj alloc] init];
obj.subObj = [[LXSubObj alloc] init];
[obj setValue:@"operate subObj's subIvar" forKeyPath:@"subObj.subIvar"];
NSLog(@"%@", [obj valueForKeyPath:@"subObj.subIvar"]);
运行结果:

键路径.png
Ps:需要注意,如果LXObj下有一组类型为LXSubObj的数组作为属性,那么NSArray实现valueForKeyPath:的方法是循环遍历它的内容并向每个对象发送信息。也就是说NSArray会向每个在自身之中的LXSubObj对象发送参数以subIvar作为键路径的valueForKeyPath:消息。
3.批处理
这个由于各位在初学iOS阶段可能就用到过,而且平时开发也会使用到,就一笔带过吧:
// 根据所给字典一一对应的设置接收消息对象内的属性值
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
// 根据数组keys一一对应的从接收消息对象内取出对应的值生成字典返回
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
4.快速运算
通过快速运算特性可以节约开发成本,简化代码,先把上面LXObj内部的subObj改为NSArray类型(这里用到了泛型语法):
@class LXSubObj;
@interface LXObj : NSObject
@property (nonatomic, strong) NSArray *subObjs;
@end
.m中初始化数组subObjs:
@implementation LXObj
- (NSArray *)subObjs
{
if (!_subObjs) {
LXSubObj *subObj1 = [[LXSubObj alloc] init];
LXSubObj *subObj2 = [[LXSubObj alloc] init];
_subObjs = @[subObj1, subObj2];
}
return _subObjs;
}
@end
然后执行下面的代码:
LXObj *obj = [[LXObj alloc] init];
NSLog(@"%@", [obj valueForKeyPath:@"subObjs.@count"]);
运行结果:

快速运算@count.png
类似@count的例子还有很多:
- @avg //获取平均数
- @min //获取最小数
- @max //获取最大数
- @distinctUnionOfObjects //过滤重复值
用kvc实现一个万能容器对象的方法
终于到的正题,这里我们为了形象一些,拿车子来举例。下面的LXCar类没有任何属性但是理论上可以存任意数量的任意类型的属性。
LXCar.h文件内代码:
@interface LXCar : NSObject
// 父老乡亲们,看清楚,这容器就是空的!!!
@end
LXCar.m内代码:
@interface LXCar ()
// 用来放置属性键值对的字典
@property (nonatomic, strong) NSMutableDictionary *mPropertiesDict;
@end
@implementation LXCar
// 没有对应key的setter方法且没有找到对应key的属性时调用
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
if (!key || [key isEqualToString:@""])
return;
if (!_mPropertiesDict) {
_mPropertiesDict = [NSMutableDictionary dictionary];
}
[_mPropertiesDict setValue:value forKey:key];
}
// 没有对应key的getter方法且没有找到对应key的属性时调用
- (id)valueForUndefinedKey:(NSString *)key
{
if (!key || [key isEqualToString:@""])
return nil;
return [_mPropertiesDict valueForKey:key];
}
@end
然后执行代码:
LXCar *car = [[LXCar alloc] init];
[car setValue:@"保时捷 卡宴 3.0T 铂金版" forKey:@"name"];
[car setValue:@"保时捷" forKey:@"brand"];
[car setValue:@"卡宴" forKey:@"categroy"];
NSLog(@"car name = %@, car brand = %@, categroy = %@", [car valueForKey:@"name"], [car valueForKey:@"brand"], [car valueForKey:@"categroy"]);
下面是执行结果,你会发现原本空空的LXCar类被我们利用KVC特性打造成了万能的容器,可以放下原本没有的name/brand/categroy属性:

执行结果.png
其实相信只要是耐着性子从头看到现在的小伙伴们都能看的懂上面的代码:利用接收对象既没有相应的setter/getter方法又无对象属性时调用- (void)setValue:(id)value forUndefinedKey:(NSString *)key以及- (id)valueForUndefinedKey:(NSString *)key两个方法的特性,给对象加入了一个可变字典作为填充属性的区域实现这样一个万能容器。
归纳kvc的优缺点
看了文章上面所讲的,你可能已经爱上KVC了。但是请清醒一下,万物都有两面性,如果滥用KVC的话也不是什么好事:
- KVC需要解析字符串来计算你所需要的答案,因此速度比较慢
- 编辑器无法对KVC进行错误检查,当你的key键输入错误时会引起运行时错误
写在最后
就像本文前面说的一样,这种万能容器对象可以用在后台接口平时还算规范但是偶尔会多返回一些出参的情况。所以不论是runtime还是KVC,是底层知识还是语言特性,一定要学以致用。毕竟空懂一腔理论知识却没有解决问题的能力学再多的东西也没有用……