KVC原理与数据筛选
作者:宋宏帅
1 前言
在技术论坛中看到一则很有意思的 KVC 案例:
interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
Person *person = [Person new];
person.name = @"Tom";
person.age = 10;
[person setValue:@"100" forKey:@"age"];//此处赋值为字符串,类中属性为Integer
第一反应是崩溃,因为 OC 是类型敏感的。可是自己实现并打印后的结果出于意料,没有崩溃且赋值成功。所以有了深入了解 KVC 的内部实现的想法!
2 什么是 KVC
key-value-coding:键值编码,一种可以通过键名间接访问和赋值对象属性的机制 KVC 是通过 NSObject、NSArray、NSDictionary 等的类别来实现的主要方法包括一下几个:
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (nullable id)valueForUndefinedKey:(NSString *)key;
3 KVC 执行分析
那么上面的案例中的- (void)setValue:(nullable id)value forKey:(NSString *)key;是怎样的执行过程呢?借助反汇编工具获得 Foundation.framework 部分源码(为了解决和系统 API 冲突问题增加前缀_d,NS 替换为 DS),以此分析 KVC 执行过程。(流程中的边界判断等已经忽略,如想了解可以参考源码,本文只探究主流程。)
3.1 设置属性
3.1.1 查找访问器方法或成员变量
+ (DSKeyValueSetter *)_d_createValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key {
DSKeyValueSetter *setter = nil;
char key_cstr_upfirst[key_cstr_len + 1];
key_cstr[key_cstr_len + 1];
...
Method method = NULL;
//按顺序寻找set<Key>,_set<Key>,setIs<Key>。找到后则生成对应的seter
if ((method = DSKeyValueMethodForPattern(self, "set%s:", key_cstr_upfirst)) ||
(method = DSKeyValueMethodForPattern(self, "_set%s:", key_cstr_upfirst)) ||
(method = DSKeyValueMethodForPattern(self, "setIs%s:", key_cstr_upfirst))
) { //生成Method:包含selector,IMP。返回和参数类型字符串
setter = [[DSKeyValueMethodSetter alloc] initWithContainerClassID:containerClassID key:key method:method];
} else if ([self accessInstanceVariablesDirectly]) {//如果没有找到对应的访问器方且工厂方法accessInstanceVariablesDirectly == ture ,则按照顺序查找查找成员变量_<key>,_is<Key>,<key>,is<Key>(注意key的首字母大小写,查找到则生成对应的setter)
Ivar ivar = NULL;
if ((ivar = DSKeyValueIvarForPattern(self, "_%s", key_cstr)) ||
(ivar = DSKeyValueIvarForPattern(self, "_is%s", key_cstr_upfirst)) ||
(ivar = DSKeyValueIvarForPattern(self, "%s", key_cstr)) ||
(ivar = DSKeyValueIvarForPattern(self, "is%s", key_cstr_upfirst))
) {
setter = [[DSKeyValueIvarSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar];
}
}
...
return setter;
}
查找顺序如下:
查找访问器方法:set,_set,setIs
如果步骤 1 中没找到对应的方法且 accessInstanceVariablesDirectly == YES
则查找顺序如下:_,_is,,is 查找不到则调用 valueForUndefinedKey 并抛出异常
3.1.2 生成 setter
+ (DSKeyValueSetter *)_d_createOtherValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key {
return [[DSKeyValueUndefinedSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self];
}
//构造方法确定方法编号 d_setValue:forUndefinedKey: 和方法指针IMP _DSSetValueAndNotifyForUndefinedKey
- (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key containerIsa:(Class)containerIsa {
...
return [super initWithContainerClassID:containerClassID key:key implementation:method_getImplementation(class_getInstanceMethod(containerIsa, @selector(d_setValue:forUndefinedKey:))) selector:@selector(d_setValue:forUndefinedKey:) extraArguments:arguments count:1];
}
3.1.3 赋值
基本的访问器方法、变量的查找和异常处理已经清楚的知道了。那么上面的例子是如何出现的呢?明明传入的是字符串,最后赋值的时候转变为访问器方法所对应的类型?继续刨根问底!
DSKeyValueSetter 对象已经生成,即确定了发送消息的对象 object、访问器方法名 SEL、访问器函数指针 IMP、以及使用 KVC 时传入的 Key 和 Value。下面进入方法调用阶段:_DSSetUsingKeyValueSetter(self,setter, value);
IMP 指针为_DSSetIntValueForKeyWithMethod 其定义如下:之所以有文章开头提到的效果就是这里起了作用,在 IMP 调用的时候做了[value valueGetSelectorName],将对应的 NSNumber 转换为简单数据类型。这里是 intValue。
void _DSSetIntValueForKeyWithMethod(id object, SEL selector,id value, NSString *key, Method method) {// object:person selector:setAge: value:@(100) key:age method:selector + IMP + 返回类型和参数类型 即_extraArgument2,其在第一步查找到访问器方法后生成
__DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, int, intValue);
}
#define __DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, valueType, valueGetSelectorName) do {\
if (value) {\
void (*imp)(id,SEL,valueType) = (void (*)(id,SEL,valueType))method_getImplementation(method);\
imp(object, method_getName(method), [value valueGetSelectorName]);\调用person的setAge:方法。参数为100
}\
else {\
[object setNilValueForKey:key];\
}\
}while(0)
//如果第一步中没有找到访问器方法只找到了成员变量则直接执行赋值操作
void _DSSetIntValueForKeyInIvar(id object, SEL selector, id value, NSString *key, Ivar ivar) {
if (value) {
*(int *)object_getIvarAddress(object, ivar) = [value intValue];
}
else {
[object setNilValueForKey:key];
}
}
起始问题完美解决!执行流程如下:
3.2 取值
3.2.1 查找访问器方法或成员变量
+ (DSKeyValueGetter *)_d_createValueGetterWithContainerClassID:(id)containerClassID key:(NSString *)key {
DSKeyValueGetter * getter = nil;
...
Method getMethod = NULL;
if((getMethod = DSKeyValueMethodForPattern(self,"get%s",keyCStrUpFirst)) ||
(getMethod = DSKeyValueMethodForPattern(self,"%s",keyCStr)) ||
(getMethod = DSKeyValueMethodForPattern(self,"is%s",keyCStrUpFirst)) ||
(getMethod = DSKeyValueMethodForPattern(self,"_get%s",keyCStrUpFirst)) ||
(getMethod = DSKeyValueMethodForPattern(self,"_%s",keyCStr))) {
getter = [[DSKeyValueMethodGetter alloc] initWithContainerClassID:containerClassID key:key method:getMethod];
}// 查找对应的访问器方法
...
else if([self accessInstanceVariablesDirectly]) {//查找属性
Ivar ivar = NULL;
if((ivar = DSKeyValueIvarForPattern(self, "_%s", keyCStr)) ||
(ivar = DSKeyValueIvarForPattern(self, "_is%s", keyCStrUpFirst)) ||
(ivar = DSKeyValueIvarForPattern(self, "%s", keyCStr)) ||
(ivar = DSKeyValueIvarForPattern(self, "is%s", keyCStrUpFirst))
) {
getter = [[DSKeyValueIvarGetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar];
}
}
}
if(!getter) {
getter = [self _d_createValuePrimitiveGetterWithContainerClassID:containerClassID key:key];
}
return getter;
}
按照 get,,is,_的顺序查找成员方法
如果 1.没有找到对应的方法且 accessInstanceVariablesDirectly==YES,则继续查找成员变量,查找顺序为_,_is,,is
如果 1,2 没有找到对应的方法和属性则调用 valueForUndefinedKey:并抛出异常
3.2.2 如上步骤没定位到访问器方法或成员变量则走下面的流程生成对应的 getter
访问器方法生成IMP
- (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key method:(Method)method {
NSUInteger methodArgumentsCount = method_getNumberOfArguments(method);
NSUInteger extraAtgumentCount = 1;
if(methodArgumentsCount == 2) {
char *returnType = method_copyReturnType(method);
IMP imp = NULL;
switch (returnType[0]) {
...
case 'i': {
imp = (IMP)_DSGetIntValueWithMethod;
} break;
...
free(returnType);
if(imp) {
void *arguments[3] = {0};
if(extraAtgumentCount > 0) {
arguments[0] = method;
}
return [super initWithContainerClassID:containerClassID key:key implementation:imp selector:method_getName(method) extraArguments:arguments count:extraAtgumentCount];
}
}
单步调试后可以看到具体的 IMP 类型
定义如下:
NSNumber * _DSGetIntValueWithMethod(id object, SEL selctor, Method method) {//
return [[[NSNumber alloc] initWithInt: ((int (*)(id,SEL))method_getImplementation(method))(object, method_getName(method))] autorelease];
}
3.2.3 取值
取值调用如下:
4 简单数据类型 KVC 包装和拆装关系
NSNunber:
NSValue
5 KVC 高级
修改数组中对象的属性[array valueForKeyPath:@”uppercaseString”]利用 KVC 可以批量修改属性的成员变量值
求和,平均数,最大值,最小值 NSNumbersum= [array valueForKeyPath:@”@sum.self”];NSNumberavg= [array valueForKeyPath:@”@avg.self”];NSNumbermax= [array valueForKeyPath:@”@max.self”];NSNumbermin= [array valueForKeyPath:@”@min.self”];
6 数据筛选
经过上面的分析可以明白 KVC 的真正执行流程。下面结合日常工程中的实际应用来优雅的处理数据筛选问题。使用 KVC 处理可以减少大量 for 的使用并增加代码可读性和健壮性。如图所示:
项目中的细节如下:修改拒收数量时更新总妥投数和总拒收数、勾选明细更新总妥投数和总拒收数、全选、清空、反选。如果用通常的做法是每次操作都要循环去计算总数和记录选择状态。下面是采用 KVC 的实现过程。模型涉及:
@property (nonatomic,copy)NSString* skuCode;
@property (nonatomic,copy)NSString* goodsName;
@property (nonatomic,assign)NSInteger totalAmount;
@property (nonatomic,assign)NSInteger rejectAmount;
@property (nonatomic,assign)NSInteger deliveryAmount;
///单选用
@property (nonatomic, assign) BOOL selected;
1)更新总数
- (void)updateDeliveryInfo {
//总数
NSNumber *allDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.totalAmount"];
//妥投数
NSNumber *allRealDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.deliveryAmount"];
//拒收数
NSNumber *allRejectAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.rejectAmount"];
}
2)全选[self.orderDetailModel.deliveryGoodsDetailList setValue:@(YES) forKeyPath:@”selected”];
3)清空[self.orderDetailModel.deliveryGoodsDetailList setValue:@(NO) forKeyPath:@”selected”];
4)反选
NSPredicate *selectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(YES)];
NSArray *selectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:selectedPredicate];
NSPredicate *unSelectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(NO)];
NSArray *unSelectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:unSelectedPredicate];
[selectedArray setValue:@(NO) forKeyPath:@"selected"];
[unSelectedArray setValue:@(YES) forKeyPath:@"selected"];
7 总结
KVC 在处理简单数据类型时会经过数据封装和拆装并转换为对应的数据类型。通过 KVC 的特性我们可以在日常使用中更加优雅的对数据进行筛选和处理。优点如下:可阅读性更高,健壮性更好。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
要想后期修改少,代码重构要趁早
摘要:在敏捷中,让设计简单化,必须让设计从简单开始,然后变得成熟。要做到这一点,重构是唯一的出路。 本文分享自华为云社区《敏捷技术实践之重构》,作者:华为云PaaS服务小智 。 前言 极限编程(XP)的创始人之一Ron Jeffries说道:“在敏捷中,让设计简单化,必须让设计从简单开始,然后变得成熟。要做到这一点,重构是唯一的出路。” 什么是重构 重构是指改变代码的结构,而不是代码的行为。举个例子:假设一个程序中有两个方法,每个方法都包含几行相同的代码,那么这几行相同的代码可以从原来的两个方法中抽取出来,放到一个新的方法中,在原来放置这几行代码的地方替换为调用这个新的方法。这个重构稍微改善了程序的可读性和可维护性,因为现在一些代码明显被复用,并且重复的代码被移到了一个地方。代码结构发生了改变,但是行为并没有变。 重构不仅对TDD(测试驱动开发)的成功至关重要,同时也有助于防止代码腐烂。代码腐烂是由重复的代码、无数的补丁、糟糕的分类和其他编程差异造成的。团队的开发人员以他们自己的风格编写代码也会导致代码腐烂。产品发布后,代码腐烂是典型的综合症,如果允许代码腐烂,过不了几年,系统就要全部...
- 下一篇
如何在几百万qps的网关服务中实现灵活调度策略
作者 | 加纳斯 导读 说起百度的BFE可能不少人都听说过,但是其实在百度内部还有一个几百万qps的通用网关服务:Janus。截止当前,Janus服务不仅覆盖了百度内部FEED、评论、点赞、关注、直播等十多个中台服务的内网流量,而且为百度app、知道、经验、passport、百科、问一问等业务提供了外网流量服务。 在百度已有BFE且BFE开源的情况下,为什么要建设Janus网关?Janus网关区别于其他网关的核心点有哪些?面对众多的接入方,如何实现既能通用又能个性化的流量调度呢? 来本文一探究竟吧。 全文3802字,预计阅读时间10分钟。 01 为什么要建设Janus 在百度已有BFE且BFE开源的情况下,为什么要建设Janus网关? 从场景上看,与BFE面向通用功能的流量网关场景不同,Janus不仅可以作为流量网关,也可以作为业务网关、混合网关;不仅面向通用功能,也支持个性化需求。从实现上看,Janus的部分技术参考了BFE,但是与厂内BFE主要提供saas类的服务不同,Janus提供的是一个通用技术方案,谁使用谁部署,谁有个性化需求谁自己定制插件。因此,Janus网关的应用场景相对...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS关闭SELinux安全模块
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Linux系统CentOS6、CentOS7手动修改IP地址