【iOS】KVC原理及自定义

目录

前言

KVC定义及API

KVC的使用

基本类型

集合类型

访问非对象类型——结构体

集合操作符

层层嵌套

KVC底层原理

设值过程

取值过程

自定义KVC

setter方法

getter方法

KVC异常小技巧

自动转换类型

设置空值

未定义的key


前言

在平时的开发中我们经常用到KVC赋值取值、字典转模型,这篇文章我们来探索一下KVC的底层原理。

KVC定义及API

KVC(Key-Value Coding)是利用NSKeyValueCoding 非正式协议实现的一种机制,对象采用这种机制来提供对其属性的间接访问。

NSKeyValueCodingFoundation框架下:

  • KVC是通过对NSObject的扩展来实现的 —— 只要继承了NSObject的类都可以使用KVC

  • NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet等也遵守KVC协议

  • 除少数类型(结构体)以外都可以使用KVC

KVC常用方法:

// 通过 key 设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通过 key 取值
- (nullable id)valueForKey:(NSString *)key;
// 通过 keyPath 设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 通过 keyPath 取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;

NSKeyValueCoding类别的其它方法:

// 默认为YES。 如果返回为YES,如果没有找到 set<Key> 方法的话, 会按照_key, _isKey, key, isKey的顺序搜索成员变量, 返回NO则不会搜索
+ (BOOL)accessInstanceVariablesDirectly;
// 键值验证, 可以通过该方法检验键值的正确性, 然后做出相应的处理
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 如果key不存在, 并且没有搜索到和key有关的字段, 会调用此方法, 默认抛出异常。两个方法分别对应 get 和 set 的情况
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// setValue方法传 nil 时调用的方法
// 注意文档说明: 当且仅当 NSNumber 和 NSValue 类型时才会调用此方法 
- (void)setNilValueForKey:(NSString *)key;
// 一组 key对应的value, 将其转成字典返回, 可用于将 Model 转成字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

KVC的使用

关于KVC的使用,其实笔者在之前已经有过很详细的分析了(详情请见博客【iOS】KVC),但是这里由于要分析KVC的源码,还是把基本的接口和用法再整理一遍

首先定义两个类方便后续使用

基本类型

对于基本类型KVC的使用,要注意NSInteger这类的属性赋值时要转成NSNumberNSString

打印的结果如下:

集合类型

打印结果:

访问非对象类型——结构体

  • 对于非对象类型的赋值总是把它先转成NSValue类型再进行存储

  • 取值时转成对应类型后再使用

打印结果:

集合操作符

聚合操作符

  • @avg: 返回操作对象指定属性的平均值

  • @count: 返回操作对象指定属性的个数

  • @max: 返回操作对象指定属性的最大值

  • @min: 返回操作对象指定属性的最小值

  • @sum: 返回操作对象指定属性值之和

数组操作符

  • @distinctUnionOfObjects: 返回操作对象指定属性的集合--去重

  • @unionOfObjects: 返回操作对象指定属性的集合

嵌套操作符

  • @distinctUnionOfArrays: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray

  • @unionOfArrays: 返回操作对象(集合)指定属性的集合

  • @distinctUnionOfSets: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSSe

层层嵌套

通过forKeyPath对实例变量(student)进行取值赋值通过forKeyPath对实例变量(student)进行取值赋值

打印结果:

KVC底层原理

设值过程

KVC底层其实就是一个按顺序查找的过程:

  1. set<Key>:_set<Key>:顺序查找对象中是否有对应的方法

  2. 判断accessInstanceVariablesDirectly结果

    1. 为YES时按照_<key>_is<Key><key>is<Key>的顺序查找成员变量,找到了就赋值;找不到就跳转第3步

    2. 为NO时跳转第3步

  3. 调用setValue:forUndefinedKey:。默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施

取值过程

取值过程是类似的流程:

  1. 按照get<Key><key>is<Key>_<key>顺序查找对象中是否有对应的方法

  2. 查找是否有countOf<Key>objectIn<Key>AtIndex: 方法(对应于NSArray类定义的原始方法)以及<key>AtIndexes: 方法(对应于NSArray方法objectsAtIndexes:)

    1. 如果找到其中的第一个(countOf<Key>),再找到其他两个中的至少一个,则创建一个响应所有 NSArray方法的代理集合对象,并返回该对象(即要么是countOf<Key> + objectIn<Key>AtIndex:,要么是countOf<Key> + <key>AtIndexes:,要么是countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:)

    2. 如果没有找到,跳转到第3步

  3. 查找名为countOf<Key>enumeratorOf<Key>memberOf<Key>这三个方法(对应于NSSet类定义的原始方法)

    1. 如果找到这三个方法,则创建一个响应所有NSSet方法的代理集合对象,并返回该对象

    2. 如果没有找到,跳转到第4步

  4. 判断accessInstanceVariablesDirectly,为YES时按照_<key>_is<Key><key>is<Key>的顺序查找成员变量,找到了就取值

  5. 判断取出的属性值

    1. 属性值是对象,直接返回

    2. 属性值不是对象,但是可以转化为NSNumber类型,则将属性值转化为NSNumber 类型返回

    3. 属性值不是对象,也不能转化为NSNumber类型,则将属性值转化为NSValue类型返回

  6. 调用valueForUndefinedKey:.默认情况下会引发一个异常,但是继承于NSObject的子类可以重写该方法就可以避免崩溃并做出相应措施

自定义KVC

我们可以自定义KVC

setter方法

首先在头文件中加入这个方法,在.m文件中引入<objc/runtime.h>这个库

然后开始实现流程,大致流程如下:

- (void)cj_setValue:(nullable id)value forKey:(NSString *)key {// 1:非空判断一下if (key == nil || key.length == 0) return;// 2:找到相关方法 set<Key> _set<Key> setIs<Key>// key 要大写NSString *Key = key.capitalizedString;// 拼接方法NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];if ([self cj_performSelectorWithMethodName:setKey value:value]) {NSLog(@"*********%@**********",setKey);return;} else if ([self cj_performSelectorWithMethodName:_setKey value:value]) {NSLog(@"*********%@**********",_setKey);return;} else if ([self cj_performSelectorWithMethodName:setIsKey value:value]) {NSLog(@"*********%@**********",setIsKey);return;}NSString *undefinedMethodName = @"setValue:forUndefinedKey:";IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));// 3:判断是否能够直接赋值实例变量if (![self.class accessInstanceVariablesDirectly]) {if (undefinedIMP) {[self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];} else {@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];}return;}// 4.找相关实例变量进行赋值// 4.1 定义一个收集实例变量的可变数组NSMutableArray *mArray = [self getIvarListName];NSString *_key = [NSString stringWithFormat:@"_%@",key];NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];NSString *isKey = [NSString stringWithFormat:@"is%@",Key];// _<key> _is<Key> <key> is<Key>if ([mArray containsObject:_key]) {// 4.2 获取相应的 ivarIvar ivar = class_getInstanceVariable([self class], _key.UTF8String);// 4.3 对相应的 ivar 设置值object_setIvar(self , ivar, value);return;} else if ([mArray containsObject:_isKey]) {Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);object_setIvar(self , ivar, value);return;} else if ([mArray containsObject:key]) {Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);object_setIvar(self , ivar, value);return;} else if ([mArray containsObject:isKey]) {Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);object_setIvar(self , ivar, value);return;}
​// 5:如果找不到相关实例if (undefinedIMP) {[self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];} else {@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];}
}

getter方法

第一步是加入库和声明方法,和setter方法相同,实现方法的过程如下:

- (nullable id)cj_valueForKey:(NSString *)key {// 1:刷选key 判断非空if (key == nil  || key.length == 0) return nil;
​// 2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex// key 要大写NSString *Key = key.capitalizedString;// 拼接方法NSString *getKey = [NSString stringWithFormat:@"get%@",Key];NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"if ([self respondsToSelector:NSSelectorFromString(getKey)]) {return [self performSelector:NSSelectorFromString(getKey)];} else if ([self respondsToSelector:NSSelectorFromString(key)]) {return [self performSelector:NSSelectorFromString(key)];} else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]) {if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];for (int i = 0; i<num-1; i++) {num = (int)[self performSelector:NSSelectorFromString(countOfKey)];}for (int j = 0; j<num; j++) {id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];[mArray addObject:objc];}return mArray;}}
#pragma clang diagnostic popNSString *undefinedMethodName = @"valueForUndefinedKey:";IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));// 3:判断是否能够直接赋值实例变量if (![self.class accessInstanceVariablesDirectly]) {if (undefinedIMP) {
​
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop} else {@throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];}}// 4.找相关实例变量进行赋值// 4.1 定义一个收集实例变量的可变数组NSMutableArray *mArray = [self getIvarListName];// _<key> _is<Key> <key> is<Key>// _name -> _isName -> name -> isNameNSString *_key = [NSString stringWithFormat:@"_%@",key];NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];NSString *isKey = [NSString stringWithFormat:@"is%@",Key];if ([mArray containsObject:_key]) {Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);return object_getIvar(self, ivar);;} else if ([mArray containsObject:_isKey]) {Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);return object_getIvar(self, ivar);;} else if ([mArray containsObject:key]) {Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);return object_getIvar(self, ivar);;} else if ([mArray containsObject:isKey]) {Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);return object_getIvar(self, ivar);;}
​if (undefinedIMP) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop} else {@throw [NSException exceptionWithName:@"FXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];}
​return nil;
}
过程几个用到的方法封装如下://安全调用方法及传两个参数
- (BOOL)cj_performSelectorWithMethodName:(NSString *)methodName value:(id)value key:(id)key {if ([self respondsToSelector:NSSelectorFromString(methodName)]) {#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"[self performSelector:NSSelectorFromString(methodName) withObject:value withObject:key];
#pragma clang diagnostic popreturn YES;}return NO;
} 
​
//安全调用方法及传一个参数
- (BOOL)cj_performSelectorWithMethodName:(NSString *)methodName value:(id)value {if ([self respondsToSelector:NSSelectorFromString(methodName)]) {#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic popreturn YES;}return NO;
}
​
//安全调用方法
- (id)performSelectorWithMethodName:(NSString *)methodName {if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"return [self performSelector:NSSelectorFromString(methodName)];
#pragma clang diagnostic pop}return nil;
}
​
//取成员变量
- (NSMutableArray *)getIvarListName {NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];unsigned int count = 0;Ivar *ivars = class_copyIvarList([self class], &count);for (int i = 0; i<count; i++) {Ivar ivar = ivars[i];const char *ivarNameChar = ivar_getName(ivar);NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];NSLog(@"ivarName == %@",ivarName);[mArray addObject:ivarName];}free(ivars);return mArray;
}

KVC异常小技巧

自动转换类型

  • int类型赋值会自动转成__NSCFNumber

  • 用结构体类型赋值会自动转成NSConcreteValue

设置空值

有时候在设值时设置空值,可以通过重写setNilValueForKey来监听,但是setNilValueForKey只对NSNumber类型有效

// Int类型设置nil
[person setValue:nil forKey:@"age"];
// NSString类型设置nil
[person setValue:nil forKey:@"subject"];
​
@implementation TCJPerson
​
- (void)setNilValueForKey:(NSString *)key {NSLog(@"设置 %@ 是空值", key);
}
​
@end

未定义的key

未定义的key可以用setValue:forUndefinedKey:valueForUndefinedKey:来监听

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

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

相关文章

完整设计 之 智能合约系统:主题约定、代理协议和智能合约 (临时命名)----PromptPilot (助手)答问之2

摘要&#xff08;CSDN的AI助手生成的&#xff09;智能合约系统架构设计摘要本设计构建了一个多层次智能合约系统&#xff0c;包含150字以内的核心架构&#xff1a;三级架构体系&#xff1a;元级&#xff08;序分&#xff09;&#xff1a;MetaModel合约定义系统核心原则模型级&a…

Java基础 8.16

1.final关键字基本介绍final中文意思&#xff1a;最后的&#xff0c;最终的final可以修饰类、属性、方法和局部变量在某些情况下&#xff0c;程序员可能有以下需求&#xff0c;就会使用到final当不希望类被继承时,可以用final修饰当不希望父类的某个方法被子类覆盖/重写(overri…

YOLOv8目标检测网络结构理论

目录 YOLOv8的网络结构图&#xff1a; Backbone 卷积块&#xff08;Conv Block&#xff09; Conv2d层 BatchNorm2d层 SiLU激活函数 瓶颈块(Bottleneck Block) C2f 模块结构 Neck SPPF(空间金字塔池化快速) PAN - FPN Head 结构1.卷积层和激活函数: 2.预测层(Predi…

docker部署hadoop集群

Docker部署hadoop集群下载资源构建镜像启动容器搭建集群配置ssh免密节点职责安排修改配置文件启动集群测试上传下载执行wordcount程序补充配置历史服务器日志聚集单节点启动Java客户端使用HDFSMapReduce下载资源 java华为镜像下载地址&#xff1a;Index of java-local/jdk (hu…

常用的T-SQL命令

文章目录1. 数据库操作2. 表操作3. 数据插入、更新、删除4. 数据查询5. 存储过程6. 事务处理7、如何使用T-SQL在表中设置主键和外键&#xff1f;1. 设置主键&#xff08;PRIMARY KEY&#xff09;方法1&#xff1a;创建表时定义主键方法2&#xff1a;通过ALTER TABLE添加主键2. …

C++面试题及详细答案100道( 31-40 )

《前后端面试题》专栏集合了前后端各个知识模块的面试题&#xff0c;包括html&#xff0c;javascript&#xff0c;css&#xff0c;vue&#xff0c;react&#xff0c;java&#xff0c;Openlayers&#xff0c;leaflet&#xff0c;cesium&#xff0c;mapboxGL&#xff0c;threejs&…

给纯小白的 Python 操作 Excel 笔记

&#x1f9f0; 1. 先装工具电脑键盘按 Win R&#xff0c;输入 cmd&#xff0c;回车&#xff0c;把下面一行粘进去回车&#xff0c;等它跑完。 bashpip install openpyxl——————————————————&#x1f6e0;️ 2. 打开一个空白的 Excel 打开 Jupyter Notebook…

HTML 常用属性介绍

目录 HTML 属性 HTML 属性速查表 一、通用属性&#xff08;所有元素适用&#xff09; 二、链接与引用相关属性 三、表单与输入控件属性 四、媒体与多媒体属性 五、事件属性&#xff08;常用 JavaScript 事件&#xff09; 六、其他常用属性 核心通用属性 id 属性 cla…

HTML5练习代码集:学习与实践核心特性

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;HTML5作为新一代网页标准&#xff0c;对Web开发提供了更丰富的功能和工具。本练习代码集专门针对HTML5的核心特性&#xff0c;包括语义化标签、离线存储、多媒体支持、图形绘制等&#xff0c;以及CSS3的3D效果和…

【RH134知识点问答题】第 10 章:控制启动过程

目录 1. 请简要说明 RHEL9 的启动过程。 2. 系统重启和关机的命令分别是什么? 3. Systemd target 是什么&#xff1f; 4. 重置丢失的 root 密码需要哪些步骤&#xff1f; 5. 如何让系统日志在重启后持久保留 1. 请简要说明 RHEL9 的启动过程。 答&#xff1a;①开机自检…

Apollo10.0学习之固态雷达与IMU的外参标定

固态雷达&#xff08;如Livox、禾赛等非旋转式激光雷达&#xff09;与IMU&#xff08;惯性测量单元&#xff09;的外参标定&#xff08;Extrinsic Calibration&#xff09;是自动驾驶、机器人定位&#xff08;如LIO-SAM、FAST-LIO&#xff09;的关键步骤。1. 标定原理 外参标定…

HTML5实现古典音乐网站源码模板1

文章目录 1.设计来源1.1 网站首页1.2 古典音乐界面1.3 著名人物界面1.4 古典乐器界面1.5 历史起源界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c;在线沟通 作者&#xff1a;xcLeigh 文章地址&#xff1a;http…

40 C++ STL模板库9-容器2-vector

C STL模板库9-容器2-vector 文章目录C STL模板库9-容器2-vector一、基础概念1. 类型成员&#xff08;Type Members&#xff09;2. 模板参数二、构造函数1. 语法2. 示例三、元素访问1. 函数说明2. 示例代码四、容量操作1. 函数说明2. 关键点说明3. 关键操作解析4. 操作示例五、修…

GPT-5系列文章2——新功能、测试与性能基准全解析

引言 2025年8月&#xff0c;OpenAI正式发布了其新一代旗舰模型GPT-5。与业界此前期待的AGI(人工通用智能)突破不同&#xff0c;GPT-5更像是OpenAI对现有技术的一次深度整合与用户体验优化。本文将全面解析GPT-5的新特性、实际测试表现以及官方发布的基准数据&#xff0c;帮助开…

利用cursor+MCP实现浏览器自动化释放双手

小伙伴们&#xff0c;我们今天利用cursorMCP实现浏览器自动化&#xff0c;释放双手&#xff0c;工作效率嘎嘎提升&#xff01;前期准备&#xff1a;安装node.js网址&#xff1a;https://nodejs.org/zh-cn下载下来安装即可。 下载browser-tools-mcp扩展程序&#xff1a;下载扩展…

指针/边界索引混淆梳理

在处理数组/链表等数据结构时&#xff0c;时常混淆长度和指针序号。处理技巧&#xff1a;使用0-base索引。则区间长度 rightIndex - LeftIndex 1总长度 lastIndex - firstIndex 1链表创建一个dummy节点&#xff0c;添加到head前&#xff0c;则可认为从索引0开始。末尾指针判…

LeetCode 刷题【43. 字符串相乘】

43. 字符串相乘 自己做 解1&#xff1a;矩阵计数 class Solution { public:string multiply(string num1, string num2) {int len1 num1.size();int len2 num2.size();if (num1[0] 0 || num2[0] 0) //结果为0的情况return "0";//存储计算过程的矩阵vector…

NLP数据增强方法及实现-A

目录 词替换 主要参考&#xff1a;paddlenlp/data_aug模块 词替换数据增强策略也即将句子中的词随机替换为其他单词进行数据增强&#xff0c;这里我们将介绍如何使用paddlenlp.dataaug.WordSubstitute进行词级别替换的数据增强。 WordSubstitute 参数介绍&#xff1a;aug_ty…

EhViewer安卓ios全版本类下载安装工具的完整路径解析

开发一款类似EhViewer的下载安装工具&#xff08;集下载管理、应用部署等功能于一体&#xff09;&#xff0c;需要经历从需求锚定到落地发布的系统性流程。以下从需求拆解到技术落地的全维度指南&#xff0c;将帮你理清开发脉络&#xff0c;避开常见陷阱。安装 GitHub - huangy…

MySQL 主键详解:作用与使用方法

在 MySQL 数据库中&#xff0c;主键&#xff08;Primary Key&#xff09; 是表结构设计中最重要的约束之一。它不仅是数据唯一性的保障&#xff0c;也是多表关联、查询优化的核心工具。本文将从 主键的作用 和 主键的用法 两个方面进行讲解&#xff0c;并配合代码示例帮助理解一…