作者:宋宏帅

1 前言

在技术论坛中看到一则很有意思的KVC案例:

  1. @interface Person : NSObject
  2. @property (nonatomic, copy) NSString *name;
  3. @property (nonatomic, assign) NSInteger age;
  4. @end
  5. Person *person = [Person new];
  6. person.name = @"Tom";
  7. person.age = 10;
  8. [person setValue:@"100" forKey:@"age"];//此处赋值为字符串,类中属性为Integer

第一反应是崩溃,因为OC是类型敏感的。可是自己实现并打印后的结果出于意料,没有崩溃且赋值成功。所以有了深入了解KVC的内部实现的想法!

2 什么是KVC

key-value-coding:键值编码,一种可以通过键名间接访问和赋值对象属性的机制
KVC是通过NSObject、NSArray、NSDictionary等的类别来实现的
主要方法包括一下几个:

  1. - (nullable id)valueForKey:(NSString *)key;
  2. - (void)setValue:(nullable id)value forKey:(NSString *)key;
  3. - (void)setNilValueForKey:(NSString *)key;
  4. - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
  5. - (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 查找访问器方法或成员变量
  1. + (DSKeyValueSetter *)_d_createValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key {
  2. DSKeyValueSetter *setter = nil;
  3. char key_cstr_upfirst[key_cstr_len + 1];
  4. key_cstr[key_cstr_len + 1];
  5. ...
  6. Method method = NULL;
  7. //按顺序寻找set<Key>,_set<Key>,setIs<Key>。找到后则生成对应的seter
  8. if ((method = DSKeyValueMethodForPattern(self, "set%s:", key_cstr_upfirst)) ||
  9. (method = DSKeyValueMethodForPattern(self, "_set%s:", key_cstr_upfirst)) ||
  10. (method = DSKeyValueMethodForPattern(self, "setIs%s:", key_cstr_upfirst))
  11. ) { //生成Method:包含selector,IMP。返回和参数类型字符串
  12. setter = [[DSKeyValueMethodSetter alloc] initWithContainerClassID:containerClassID key:key method:method];
  13. } else if ([self accessInstanceVariablesDirectly]) {//如果没有找到对应的访问器方且工厂方法accessInstanceVariablesDirectly == ture ,则按照顺序查找查找成员变量_<key>,_is<Key>,<key>,is<Key>(注意key的首字母大小写,查找到则生成对应的setter)
  14. Ivar ivar = NULL;
  15. if ((ivar = DSKeyValueIvarForPattern(self, "_%s", key_cstr)) ||
  16. (ivar = DSKeyValueIvarForPattern(self, "_is%s", key_cstr_upfirst)) ||
  17. (ivar = DSKeyValueIvarForPattern(self, "%s", key_cstr)) ||
  18. (ivar = DSKeyValueIvarForPattern(self, "is%s", key_cstr_upfirst))
  19. ) {
  20. setter = [[DSKeyValueIvarSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar];
  21. }
  22. }
  23. ...
  24. return setter;
  25. }

查找顺序如下:

  1. 查找访问器方法:set,_set,setIs
  2. 如果步骤1中没找到对应的方法且 accessInstanceVariablesDirectly == YES

则查找顺序如下:_,_is,,is
查找不到则调用valueForUndefinedKey并抛出异常

3.1.2 生成setter
  1. + (DSKeyValueSetter *)_d_createOtherValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key {
  2. return [[DSKeyValueUndefinedSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self];
  3. }
  4. //构造方法确定方法编号 d_setValue:forUndefinedKey: 和方法指针IMP _DSSetValueAndNotifyForUndefinedKey
  5. - (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key containerIsa:(Class)containerIsa {
  6. ...
  7. 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];
  8. }

3.1.3 赋值

基本的访问器方法、变量的查找和异常处理已经清楚的知道了。那么上面的例子是如何出现的呢?明明传入的是字符串,最后赋值的时候转变为访问器方法所对应的类型?继续刨根问底!

DSKeyValueSetter对象已经生成,即确定了发送消息的对象object、访问器方法名SEL、访问器函数指针IMP、以及使用KVC时传入的Key和Value。下面进入方法调用阶段:_DSSetUsingKeyValueSetter(self,setter, value);

IMP指针为_DSSetIntValueForKeyWithMethod其定义如下:之所以有文章开头提到的效果就是这里起了作用,在IMP调用的时候做了[value valueGetSelectorName],将对应的NSNumber转换为简单数据类型。这里是intValue。

  1. void _DSSetIntValueForKeyWithMethod(id object, SEL selector,id value, NSString *key, Method method) {// object:person selector:setAge: value:@(100) key:age method:selector + IMP + 返回类型和参数类型 即_extraArgument2,其在第一步查找到访问器方法后生成
  2. __DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, int, intValue);
  3. }
  4. #define __DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, valueType, valueGetSelectorName) do {\
  5. if (value) {\
  6. void (*imp)(id,SEL,valueType) = (void (*)(id,SEL,valueType))method_getImplementation(method);\
  7. imp(object, method_getName(method), [value valueGetSelectorName]);\调用person的setAge:方法。参数为100
  8. }\
  9. else {\
  10. [object setNilValueForKey:key];\
  11. }\
  12. }while(0)
  13. //如果第一步中没有找到访问器方法只找到了成员变量则直接执行赋值操作
  14. void _DSSetIntValueForKeyInIvar(id object, SEL selector, id value, NSString *key, Ivar ivar) {
  15. if (value) {
  16. *(int *)object_getIvarAddress(object, ivar) = [value intValue];
  17. }
  18. else {
  19. [object setNilValueForKey:key];
  20. }
  21. }

起始问题完美解决!执行流程如下:

3.2 取值

3.2.1 查找访问器方法或成员变量
  1. + (DSKeyValueGetter *)_d_createValueGetterWithContainerClassID:(id)containerClassID key:(NSString *)key {
  2. DSKeyValueGetter * getter = nil;
  3. ...
  4. Method getMethod = NULL;
  5. if((getMethod = DSKeyValueMethodForPattern(self,"get%s",keyCStrUpFirst)) ||
  6. (getMethod = DSKeyValueMethodForPattern(self,"%s",keyCStr)) ||
  7. (getMethod = DSKeyValueMethodForPattern(self,"is%s",keyCStrUpFirst)) ||
  8. (getMethod = DSKeyValueMethodForPattern(self,"_get%s",keyCStrUpFirst)) ||
  9. (getMethod = DSKeyValueMethodForPattern(self,"_%s",keyCStr))) {
  10. getter = [[DSKeyValueMethodGetter alloc] initWithContainerClassID:containerClassID key:key method:getMethod];
  11. }// 查找对应的访问器方法
  12. ...
  13. else if([self accessInstanceVariablesDirectly]) {//查找属性
  14. Ivar ivar = NULL;
  15. if((ivar = DSKeyValueIvarForPattern(self, "_%s", keyCStr)) ||
  16. (ivar = DSKeyValueIvarForPattern(self, "_is%s", keyCStrUpFirst)) ||
  17. (ivar = DSKeyValueIvarForPattern(self, "%s", keyCStr)) ||
  18. (ivar = DSKeyValueIvarForPattern(self, "is%s", keyCStrUpFirst))
  19. ) {
  20. getter = [[DSKeyValueIvarGetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar];
  21. }
  22. }
  23. }
  24. if(!getter) {
  25. getter = [self _d_createValuePrimitiveGetterWithContainerClassID:containerClassID key:key];
  26. }
  27. return getter;
  28. }
  1. 按照get,,is,_的顺序查找成员方法
  2. 如果1.没有找到对应的方法且accessInstanceVariablesDirectly==YES,则继续查找成员变量,查找顺序为_,_is,,is
  3. 如果1,2没有找到对应的方法和属性则调用 valueForUndefinedKey:并抛出异常
3.2.2 如上步骤没定位到访问器方法或成员变量则走下面的流程生成对应的getter
  1. 访问器方法生成IMP
  2. - (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key method:(Method)method {
  3. NSUInteger methodArgumentsCount = method_getNumberOfArguments(method);
  4. NSUInteger extraAtgumentCount = 1;
  5. if(methodArgumentsCount == 2) {
  6. char *returnType = method_copyReturnType(method);
  7. IMP imp = NULL;
  8. switch (returnType[0]) {
  9. ...
  10. case 'i': {
  11. imp = (IMP)_DSGetIntValueWithMethod;
  12. } break;
  13. ...
  14. free(returnType);
  15. if(imp) {
  16. void *arguments[3] = {0};
  17. if(extraAtgumentCount > 0) {
  18. arguments[0] = method;
  19. }
  20. return [super initWithContainerClassID:containerClassID key:key implementation:imp selector:method_getName(method) extraArguments:arguments count:extraAtgumentCount];
  21. }
  22. }

单步调试后可以看到具体的IMP类型

定义如下:

  1. NSNumber * _DSGetIntValueWithMethod(id object, SEL selctor, Method method) {//
  2. return [[[NSNumber alloc] initWithInt: ((int (*)(id,SEL))method_getImplementation(method))(object, method_getName(method))] autorelease];
  3. }
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的实现过程。
模型涉及:

  1. @property (nonatomic,copy)NSString* skuCode;
  2. @property (nonatomic,copy)NSString* goodsName;
  3. @property (nonatomic,assign)NSInteger totalAmount;
  4. @property (nonatomic,assign)NSInteger rejectAmount;
  5. @property (nonatomic,assign)NSInteger deliveryAmount;
  6. ///单选用
  7. @property (nonatomic, assign) BOOL selected;

1)更新总数

  1. - (void)updateDeliveryInfo {
  2. //总数
  3. NSNumber *allDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.totalAmount"];
  4. //妥投数
  5. NSNumber *allRealDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.deliveryAmount"];
  6. //拒收数
  7. NSNumber *allRejectAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.rejectAmount"];
  8. }

2)全选
[self.orderDetailModel.deliveryGoodsDetailList setValue:@(YES) forKeyPath:@”selected”];

3)清空
[self.orderDetailModel.deliveryGoodsDetailList setValue:@(NO) forKeyPath:@”selected”];

4)反选

  1. NSPredicate *selectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(YES)];
  2. NSArray *selectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:selectedPredicate];
  3. NSPredicate *unSelectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(NO)];
  4. NSArray *unSelectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:unSelectedPredicate];
  5. [selectedArray setValue:@(NO) forKeyPath:@"selected"];
  6. [unSelectedArray setValue:@(YES) forKeyPath:@"selected"];

7 总结

KVC在处理简单数据类型时会经过数据封装和拆装并转换为对应的数据类型。通过KVC的特性我们可以在日常使用中更加优雅的对数据进行筛选和处理。优点如下:可阅读性更高,健壮性更好。

KVC原理与数据筛选的更多相关文章

  1. 【原】iOS学习之KVC原理

    1. KVC的实现原理 遍历字典里面所有的key,以name为例 去模型中查找有没有setName:方法,有就直接调用赋值 假如没有找到setName:方法,就会继续查找有没有_name属性,有就_n ...

  2. ASP.NET MVC5+EF6+EasyUI 后台管理系统(81)-数据筛选(万能查询)

    系列目录 前言 听标题的名字似乎是一个非常牛X复杂的功能,但是实际上它确实是非常复杂的,我们本节将演示如何实现对数据,进行组合查询(数据筛选) 我们都知道Excel中是如何筛选数据的.就像下面一样 他 ...

  3. DataGridView如何实现列标头带数据筛选功能,就象Excel高级筛选功能一样

    '近日有本论坛网友问:DataGridView如何实现列标头带数据筛选功能,就象Excel高级筛选功能一样 '今晚正好闲着没事,加之以前也没用到过这个需求,所以就写了个模拟功能,供各位坛友酌情参考. ...

  4. layui table 根据条件改变更换表格颜色 高亮显示 数据筛选

    请问想让当layui表格的某个字段符合某个条件的时候,让该行变颜色.这样可以实现么. layui数据表格怎么更换表格颜色 layui表格 通过判断某一行中的某一列的值进行设置这一行的颜色 LayUI之 ...

  5. C#进行数据筛选(二)

    这里介绍LINQ+Lambda表达式进行数据筛选的方式 这里是第一种方式,还是使用了if条件语句去判断,根据选择的条件去筛选出我所需要的数据 public GxAnaly SelectDay(stri ...

  6. C#进行数据筛选(一)

    这里介绍数据筛选的第一种方式,不用过滤器,给新手看得 public DataTable SourceList(string Wmain, string OrderNo, string Process) ...

  7. MySQL 分区表原理及数据备份转移实战

    MySQL 分区表原理及数据备份转移实战 1.分区表含义 分区表定义指根据可以设置为任意大小的规则,跨文件系统分配单个表的多个部分.实际上,表的不同部分在不同的位置被存储为单独的表.用户所选择的.实现 ...

  8. python之pandas数据筛选和csv操作

    本博主要总结DaraFrame数据筛选方法(loc,iloc,ix,at,iat),并以操作csv文件为例进行说明 1. 数据筛选 a b c (1)单条件筛选 df[df[] # 如果想筛选a列的取 ...

  9. Pandas 数据筛选,去重结合group by

    Pandas 数据筛选,去重结合group by 需求 今小伙伴有一个Excel表, 是部门里的小伙9月份打卡记录, 关键字段如下: 姓名, 工号, 日期, 打卡方式, 时间, 详细位置, IP地址. ...

  10. 【杂记】mysql 左右连接查询中的NULL的数据筛选问题,查询NULL设置默认值,DATE_FORMAT函数

    MySQL左右连接查询中的NULL的数据筛选问题 xpression 为 Null,则 IsNull 将返回 True:否则 IsNull 将返回 False. 如果 expression 由多个变量 ...

随机推荐

  1. BUUCTF Misc 被偷走的文件

    首先下载文件打开 得到一个流量文件 用wireshark打开 打开后 进行分析 看到有ftp流量,于是过滤ftp 看到被偷走的是flag.rar 接下用binwalk进行分离 binwalk -e f ...

  2. Spring 后置处理器【1】

    Spring 后置处理器[1] 简单介绍 一句话:bean 在初始化前或初始化后的瞬间,我自己添加一些业务逻辑 bean 后置处理器类的内容 简单代码 package com.hspedu.sprin ...

  3. 1_Maven

    一. 引言 1.1 项目管理问题 项目中jar包资源越来越多, jar包的管理越来越沉重 1.1.1 繁琐 要为每个项目手动导入所需的jar, 需要搜集全部的jar 1.1.2 复杂 项目中的jar如 ...

  4. MiniWord .NET Word模板引擎,藉由Word模板和数据简单、快速生成文件。

    Github / Gitee QQ群(1群) : 813100564 / QQ群(2群) : 579033769 介绍 MiniWord .NET Word模板引擎,藉由Word模板和数据简单.快速生 ...

  5. 洛谷P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G (tarjan缩点)

    在本题中很明显,给你一个有向图,要用tarjan缩点. 缩点后,一头牛要受到所有牛的欢迎,那么该点的出度要为0,这是容易证明的:如果该点还有出度,比如a连向b,那么a不受到b的欢迎.所以我们要找出度为 ...

  6. 代码随想录第七天| 454.四数相加II、383. 赎金信 、15. 三数之和 、18. 四数之和

    第一题454.四数相加II 给你四个整数数组 nums1.nums2.nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足: 0 <= i, ...

  7. MatrixOne从入门到实践02——源码编译

    MatrixOne从入门到实践--源码编译 ​ 在部署MatrixOne前,我们可能会比较纠结使用哪个版本合适,MatrixOne在github上有各个版本的Releases,包含源码包和适用于Lin ...

  8. 关于maven插件在Myeclipse中的使用

    出现了如下几个错误: 1.访问不到maven的中央仓库,插件下载失败,无法使用插件启动tomcat命令. 解决方法到 maven的配置文件中,一般在我的文档中的.m2文件中的settting.xml中 ...

  9. 【算法训练营day1】LeetCode704. 二分查找 LeetCode27. 移除元素

    [算法训练营day1]LeetCode704. 二分查找 LeetCode27. 移除元素 LeetCode704. 二分查找 题目链接:704. 二分查找 初次尝试 看到题目标题是二分查找,所以尝试 ...

  10. 小程序返回上一级页面背景音乐报错 setBackgroundAudioState:fail title is nil!;

    小程序初始化在onLoad的时候加载了一次背景音乐. 如果此时报错是title必传.如果没有 会报错一次 setBackgroundAudioState:fail title is nil!; 这个都 ...