当前位置: 首页 > news >正文

【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.lryc.cn/news/620527.html

相关文章:

  • 【KALI】第一篇 安装Kali Linux虚拟机之详细操作步骤讲解
  • Redis 从入门到生产:数据结构、持久化、集群、工程实践与避坑(含 Node.js/Python 示例)
  • Windows 安装 Claude Code 并将 Claude Code 的大模型替换为 Kimi 的完整步骤
  • 适用工业分选和工业应用的高光谱相机有哪些?什么品牌比较好?
  • 如何写出更清晰易读的布尔逻辑判断?
  • 【奔跑吧!Linux 内核(第二版)】第7章:系统调用的概念
  • 基于Java飞算AI的Spring Boot聊天室系统全流程实战
  • 在FP32输入上计算前向传播需要多长时间?FP16模型的实例与之前的模型相比,它快了多少?
  • 解刨HashMap的put流程 <二> JDK 1.8
  • 【自动驾驶】自动驾驶概述 ① ( 自动驾驶 与 无人驾驶 | 自动驾驶 相关岗位 及 技能需求 )
  • Day58--图论--117. 软件构建(卡码网),47. 参加科学大会(卡码网)
  • 从零开始的云计算生活——激流勇进,kubernetes模块之Pod资源对象
  • 解决EKS中KEDA访问AWS SQS权限问题:完整的IRSA配置指南
  • 【web站点安全开发】任务4:JavaScript与HTML/CSS的完美协作指南
  • 【论文阅读】基于卷积神经网络和预提取特征的肌电信号分类
  • 随身 Linux 开发环境:使用 cpolar 内网穿透服务实现 VSCode 远程访问
  • docker使用指定的MAC地址启动podman使用指定的MAC地址启动
  • vllmsglang 单端口多模型部署方案
  • 用飞算JavaAI一键生成电商平台项目:从需求到落地的高效实践
  • Java中加载语义模型
  • 【无标题】卷轴屏手机前瞻:三星/京东方柔性屏耐久性测试进展
  • 2025年世界职业院校技能大赛:项目简介模板
  • 工业一体机5G通讯IC/ID刷卡让MES系统管理更智能
  • SpringBoot 实现在线查看内存对象拓扑图 —— 给 JVM 装上“透视眼”
  • PostgreSQL + TimescaleDB 数据库语法配置
  • C++状态模式详解:从OpenBMC源码看架构、原理与应用
  • linux 下第三方库编译及交叉编译——MDBTOOLS--arm-64
  • uni-app 小程序跳转小程序
  • 《多级缓存架构设计与实现全解析》
  • Canon PowerShot D30相机 CHDK 固件 V1.4.1