KVC原理与数据筛选
创始人
2024-02-28 13:43:41
0

作者:宋宏帅

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,_set,setIs。找到后则生成对应的seterif ((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 ,则按照顺序查找查找成员变量_,_is,,is(注意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;
}

查找顺序如下:

  1. 查找访问器方法:set,_set,setIs
  2. 如果步骤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;
}
  1. 按照get,is,_的顺序查找成员方法
  2. 如果1.没有找到对应的方法且accessInstanceVariablesDirectly==YES,则继续查找成员变量,查找顺序为_,_is,is
  3. 如果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的特性我们可以在日常使用中更加优雅的对数据进行筛选和处理。优点如下:可阅读性更高,健壮性更好。

相关内容

热门资讯

《终结者2》的经典台词 《终结者2》的经典台词  1.I’ll be back。  我会回来的。  2.I need you...
婚宴新娘致辞 婚宴新娘致辞(合集15篇)  无论在学习、工作或是生活中,大家都对致辞很是熟悉吧,在各种重大的庆典、...
单位领导证婚词 单位领导证婚词尊敬的各位来宾、各位亲朋好友,女士们、先生们:大家中--午--好!今天,艳阳高照,天赐...
诵经典唱红歌主持词 诵经典唱红歌主持词  女:各位领导、各位来宾,  男:老师们、同学们,  合:大家好!  女:五月良...
笑傲江湖之东方不败台词 笑傲江湖之东方不败台词大全  天下风云出我辈,  一入江湖岁月催。  皇图霸业谈笑中,  不胜人生一...
学校元宵联欢晚会的主持词 学校元宵联欢晚会的主持词(精选6篇)  利用在中国拥有几千年文化的诗词能够有效提高主持词的感染力。我...
新婚婚礼主持词 新婚婚礼主持词  主持词的写作要突出活动的主旨并贯穿始终。我们眼下的社会,各种集会的节目都通过主持人...
酒店开业致辞 酒店开业致辞(精选20篇)  在日常学习、工作抑或是生活中,大家对致辞都再熟悉不过了吧,致辞具有有张...
主持人台词 主持人台词大全  导语:在剧作中,台词是一种重要的手段和方法,可以用来刻画人物的性格特征,可以用来展...
封顶仪式致辞 封顶仪式致辞  在平凡的学习、工作、生活中,大家都写过致辞吧,致辞具有很强的实用性和针对性。究竟什么...
唯美中式婚礼主持词 唯美中式婚礼主持词五篇  活动对象的不同,主持词的写作风格也会大不一样。我们眼下的社会,主持词是活动...
商会会长中秋致辞 商会会长中秋致辞尊敬的各位领导、各位来宾、女士们、先生们:大家好! 金风送爽、春华秋实,又是一年一度...
学校庆祝教师节座谈会主持词 2021年学校庆祝教师节座谈会主持词  根据活动对象的不同,需要设置不同的主持词。在当今中国社会,主...
主持的谢幕词 有关主持的谢幕词(通用10篇)  导语:谢幕是指演出结束后观众鼓掌时,演员站在台上向观众弯腰敬礼答谢...
一年级新队员入队仪式主持词 一年级新队员入队仪式主持词  主持词是各种演出活动和集会中主持人串联节目的串联词。在人们越来越多的参...
《老表,你好Hea》经典台词 《老表,你好Hea》经典台词  1.我是林在野,我的人生只有一个目标,就是抗议。——林在野  2.我...
艺术节闭幕式闭幕词 艺术节闭幕式闭幕词(通用6篇)  契合现场环境的闭幕词能给集会带来双倍的效果。在如今这个中国,闭幕词...
元旦茶话会主持词 元旦茶话会主持词6篇  一年的新年的钟声即将敲响,时光的车轮又留下了一道深深的印痕。有很多学校班级或...
入队仪式主持词 入队仪式主持词  一、什么是主持词  由主持人于节目进行过程中串联节目的串联词。如今的各种演出活动和...
幼儿园元旦文艺汇演节目串词 幼儿园元旦文艺汇演节目串词幼儿园2011年元旦文艺汇演节目串词a:尊敬的各位领导、各位家长b:亲爱的...