KVC原理与数据筛选

news2024/10/7 10:16:47

作者:宋宏帅

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;
}

查找顺序如下:

  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的特性我们可以在日常使用中更加优雅的对数据进行筛选和处理。优点如下:可阅读性更高,健壮性更好。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/45665.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[附源码]计算机毕业设计springboot基于JavaWeb的学校社团活动管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

小知识· Zigbee 简介

1. 介绍 ZigBee是一种近距离、低复杂度、低功耗、低速率、低成本的双向无线通讯技术 ZigBee建立在IEEE 802.15.4标准&#xff08;定义了PHY和MAC层&#xff09;之上&#xff0c;ZigBee联盟对其网络层和应用层进行了标准化 ZigBee协议栈可分为五层 - 物理层&#xff08;PHY&a…

时间序列建模三部曲

与大多数高级分析解决方案不同&#xff0c;时间序列建模是一种低成本解决方案&#xff0c;可提供强大的洞察力。 本文将介绍构建质量时间序列模型的三个基本步骤&#xff1a;使数据平稳&#xff0c;选择正确的模型并评估模型的准确性。这篇文章中的例子使用了一家主要汽车营销…

[附源码]Python计算机毕业设计SSM旅行网的设计与实现(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

从零搭建开发脚手架 注意Logback多个配置文档导致配置紊乱问题

文章目录背景查找logback内部状态以及生效的配置文件解决背景 最近项目中出现个问题&#xff0c;自定义的logback.xml不生效&#xff0c;排查发现项目中出现了2个logback.xml&#xff0c;另一个在依赖Jar中&#xff0c;使用的在依赖Jar中的logback.xml&#xff0c;导致我们自定…

Azkaban源码阅读与本地调试

1、架构 2、本地源码加载,gradle会下载依赖。 AzkabanWebServer 配置 参数如下: -Dlog4j.configuration=file:///D:\workspace_com\azkaban\azkaban\azkaban-web-server\src\main\resources\conf\log4j.properties -conf D:\\workspace_com\\azkaban\\azkaban\\azkaban-…

Unity实现摄像头录像功能

Unity实现摄像头录像功能 前言 在之前的很多展馆展示的项目中&#xff0c;甲方有很多要求实现用摄像头录像的功能。使用Unity实现调用USB摄像头画面的功能非常容易实现&#xff0c;但是实现录屏的功能有一些困难&#xff0c;我使用了几种方法都没有实现出想要的效果&#xff…

广播实现强制下线功能

实现强制下线功能 强制下线应该是一个比较常用的功能,比如QQ在比的地方被登陆了,就会强制比被挤下线.强制下线的功能还是比较简单的,只需要在界面上弹出一个框,告知用户无法再进行任何操作即可.只能点击确定然后跳转至登录界面.强制下线功能需要关闭所有的Activity,然后返回到…

5 - 2 单选题

1.下列线索二叉树中&#xff08;用虚线表示线索&#xff09;&#xff0c;符合后序线索树定义的是&#xff1a;B 后序线索二叉树的构建流程就是&#xff1a; 1.后序遍历二叉树&#xff1a;d b c a 2.第一个结点的前驱是NULL&#xff0c;即d的前驱&#xff0c;d的左孩子为NULL …

在 Android 中创建静态应用程序快捷方式

您是否在日常应用程序中看到过快捷方式示例?可能像 Instagram、Discord、Medium 等。提供它们是为了帮助我们快速导航到应用程序内部的特定功能、屏幕或部分,而无需逐步浏览它们。 在本文中,我们将逐步实现静态快捷方式。但在我们开始编码之前,我们需要知道这个快捷方式到底…

大数据(9h)FlinkSQL之Lookup Join

文章目录概述pom.xmlMySQL建表对应Flink的建表SQLLookup JoinFlinkSQL完整Java代码概述 lookup join通常是 查询外部系统的数据 来 充实FlinkSQL的主表 例如&#xff1a;事实表 关联 维度表&#xff0c;维度表在外部系统&#xff08;如MySQL&#xff09;要求&#xff1a; 1个表…

中国多媒体与网络教学学报杂志社中国多媒体与网络教学学报编辑部2022年第9期目录

多媒体信息技术《中国多媒体与网络教学学报》投稿&#xff1a;cn7kantougao163.com 采油工程探索式虚拟仿真实验教学实践——以有杆抽油系统实验为例 窦祥骥 ;何岩峰 ;张少辉 ;王相 ;徐慧 ; 1-5 人体寄生虫课程网络虚拟实验环境的构建及其应用研究 周蕾;贺帅;李晓琳;席…

深入理解mysql执行的底层机制

MySql系列整体栏目 内容链接地址【一】深入理解mysql索引本质https://blog.csdn.net/zhenghuishengq/article/details/121027025【二】深入理解explain以及索引优化https://blog.csdn.net/zhenghuishengq/article/details/124552080【三】深入理解mysql事务本质https://blog.cs…

3dmax如何进行网络渲染?网渲云渲染渲染农场怎么用?

渲染本身是将3d模型转换为2d图像的一个过程&#xff0c;而网络渲染就是把3d模型放在云端进行完成&#xff0c;而本地我们只需要等待结果就好。而云渲染也就是网渲的标准称呼&#xff0c;两个是一个意思。 那怎么进行网络渲染呢&#xff1f; 首先我们需要下载网络渲染客户端&a…

03-Docker-Docker镜像的分层概念

目录 一、镜像是什么 二、UnionFS&#xff08;联合文件系统&#xff09; 三、Docker镜像加载原理 四、将容器生成为镜像Commit命令 一、镜像是什么 是一种轻量级、可执行的独立软件包&#xff0c;包含运行某个软件所需的所有内容&#xff0c;我们把应用程序和配置依赖打包好…

TextBox文本框与PasswordBox密码框水印

在开发一个软件和网页的时候&#xff0c;都会有一个功能&#xff0c;那就是登陆功能&#xff0c;有了登陆那就一定需要用户输入账号和密码&#xff0c;我们在写登陆页面都会想到使用TextBox和PasswordBox去完成这两个功能&#xff0c;但是有一个问题&#xff0c;那就是如果你使…

java EE初阶 — 线程的状态

文章目录1.状态的基本认识2.观察线程的所有状态3.线程状态和状态转移4.多线程的意义1.状态的基本认识 NEW 创建了 Thread 对象&#xff0c;但是还没调用 start&#xff08;内核里还没有创建对应的PCB&#xff09;TERMINATED 表示内核中的 PCB 已经执行完毕了&#xff0c;但是 …

zabbix监控触发器与报警动作

目录 一、环境准备 1、搭建zabbix基础环境 2、创建被监控主机 二、触发器概念 三、创建触发器 1、创建触发器步骤 2、触发器表达式 &#xff08;1&#xff09;表达式格式 &#xff08;2&#xff09;表达式函数 3、配置触发器 四、创建报警动作 1、设置邮箱服务器 …

学生选课系统

项目描述 通过项目背景的分析以及了解到现在学校面临的问题&#xff0c;特别需要一个选课管理系统保证学生信息以及各种课程成绩的准确性和实效性&#xff0c;通过利用计算机的高速计算和快速的统计分析&#xff0c;保证学生信息的最新记录。从教职工的角度老考虑&#xff0c;…

网络套接字编程(UDP协议)

文章目录预备知识socket&#xff08;网络套接字&#xff09;编程接口简单的UDP网络程序增加多用户可以互相通信预备知识 网络字节序 大端存储&#xff1a;数据的高字节内容保存在内存的低地址处&#xff0c;数据的低字节内容保存在内存的高地址处 小端存储&#xff1a;数据的高…