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

「iOS」——KVC

源码学习

  • iOS底层学习:KVC 底层原理
    • 一、核心 API 与功能特性
      • **常用方法**
    • KVC 设值 底层原理
    • KVC 取值 底层原理
    • 自定义KVC
      • 设值
      • 取值
      • **特性:无隐私访问**
        • **原理**
    • 四、多元应用场景
      • 1. **动态数据处理**
        • (1)字典转模型
        • (2)模型转字典
      • 2. **系统控件定制**
      • 3. **高阶集合操作**
      • **(1) 聚合运算**
      • **(2) 数组映射**
      • **(3) 嵌套集合操作**


iOS底层学习:KVC 底层原理

在 iOS 开发中,KVC(Key-Value Coding,键值编码) 是一种通过字符串键名间接访问对象属性的机制,基于NSKeyValueCoding协议实现。它提供了动态访问对象属性的能力,无需直接调用存取方法或操作实例变量,极大提升了代码的灵活性和动态性。

一、核心 API 与功能特性

KVC 提供了一套完整的 API 用于属性的读写、验证和集合操作,核心方法如下:

常用方法

  • 通过key 设值/取值
    • 方法原型
//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;

​ 使用:

// 通过键名获取属性值
id value = [object valueForKey:@"propertyKey"]; 
// 通过键路径获取嵌套属性值(如:person.student.name)
id value = [object valueForKeyPath:@"keyPath"]; 
  • 通过keyPath (即路由)设值/取值

    • 方法原型
    //通过KeyPath来取值
    - (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来设值                 
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  
    

    使用

    // 通过键名设置属性值
    [object setValue:newValue forKey:@"propertyKey"]; 
    // 通过键路径设置嵌套属性值
    [object setValue:newValue forKeyPath:@"keyPath"]; 
    

其他方法

//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;//和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

通过 validateValue:forKey:error: 方法验证属性值的合法性:

- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError {// 自定义验证逻辑(如:限制数值范围、字符串长度等)if ([inKey isEqualToString:@"age"] && [(NSNumber *)*ioValue integerValue] < 0) {*ioValue = @(0); // 修正非法值if (outError) {*outError = [NSError errorWithDomain:@"ValidationError" code:0 userInfo:@{NSLocalizedDescriptionKey:@"Age cannot be negative"}];}return NO;}return YES;
}

KVC 设值 底层原理

在日常开发中,针对对象属性的赋值,一般有以下两种方式

  • 直接通过setter方法赋值
  • 通过KVC键值编码的相关API赋值
LGPerson *person = [[LGPerson alloc] init];
// 1、一般setter 方法
person.name      = @"CJL_哈哈";
// 2、KVC方式
[person setValue:@"CJL_嘻嘻" forKey:@"name"]; 

下面针对使用最多的KVC设值方法:setValue:forKey,来进行其底层原理的探索。

首先进入setValue:forKey的声明,发现是在Foundation框架中,而Foundation框架是不开源的,所以我们通过观看官方文档研究。

当调用setValue:forKey:设置属性value时,其底层的执行流程为

  • 【第一步】首先查找是否有这三种setter方法,按照查找顺序为set<Key>:-> _set<Key> -> setIs<Key>

    • 如果有其中任意一个setter方法,则直接设置属性的value(注意:key是指成员变量名,首字符大小写需要符合KVC的命名规范)
    • 如果都没有,则进入【第二步】
  • 【第二步】:如果没有第一步中的三个简单的setter方法,则查找accessInstanceVariablesDirectly是否返回YES

    • 如果返回YES

      ,则查找间接访问的实例变量进行赋值,查找顺序为:_<key> -> _is<Key> -> <key> -> is<Key>

      • 如果找到其中任意一个实例变量,则赋值
      • 如果都没有,则进入【第三步】
    • 如果返回NO,则进入【第三步】

  • 【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

综上所述,KVC通过 setValue:forKey: 方法设值的流程以设置LGPerson的对象person的属性name为例,如下图所示

请添加图片描述

KVC 取值 底层原理

同样的,我们可以通过官方文档分析KVC取值的底层原理

当调用valueForKey:时,其底层的执行流程如下

  • 【第一步】首先查找getter方法,按照

    get<Key> -> <key> -> is<Key> -> _<key>
    

    的方法顺序查找,

    • 如果找到,则进入【第五步】
    • 如果没有找到,则进入【第二步】
  • 【第二步】如果第一步中的getter方法没有找到,KVC会查找. countOf <Key>和objectIn <Key> AtIndex :和<key> AtIndexes

    • 如果找到countOf <Key>和其他两个中的一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即NSKeyValueArray,是NSArray子类。代理对象随后将接收到的所有NSArray消息转换为countOf<Key>,objectIn<Key> AtIndex:和<key>AtIndexes:消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get<Key>:range:之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)
    • 如果没有找到这三个访问数组的,请继续进入【第三步】
  • 【第三步】如果没有找到上面的几种方法,则会同时查找countOf <Key>,enumeratorOf<Key>和memberOf<Key>这三个方法

    • 如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOf<Key>,enumeratorOf<Key>和memberOf<Key>:消息的某种组合,用于创建它的对象
    • 如果还是没有找到,则进入【第四步】
  • 【第四步】如果还没有找到,检查类方法InstanceVariablesDirectly是否YES,依次搜索 _<key>,_is<Key>,<key>或is<Key>的实例变量

    • 如果搜到,直接获取实例变量的值,进入【第五步】
  • 【第五步】根据搜索到的属性值的类型,返回不同的结果

    • 如果是对象指针,则直接返回结果
    • 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它
    • 如果是是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象
  • 【第六步】如果上面5步的方法均失败,系统会执行该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

综上所述,KVC通过 valueForKey: 方法取值的流程以设置LGPerson的对象person的属性name为例,如下图所示

请添加图片描述

自定义KVC

根据上述KVC设值和取值的逻辑,我们可以写一个自定义的KVC。

设值

自定义KVC设置流程,主要分为以下几个步骤:

  • 1、判断key非空
  • 2、查找setter方法,顺序是:setXXX、_setXXX、 setIsXXX
  • 3、判断是否响应accessInstanceVariablesDirectly方法,即间接访问实例变量,
    • 返回YES,继续下一步设值,
    • 如果是NO,则崩溃
  • 4、间接访问变量赋值(只会走一次),顺序是:_key、_isKey、key、isKey
    • 4.1 定义一个收集实例变量的可变数组
    • 4.2 通过class_getInstanceVariable方法,获取相应的 ivar
    • 4.3 通过object_setIvar方法,对相应的 ivar 设置值
  • 5、如果找不到相关实例变量,则抛出异常
@interface NSObject (GCKVC)//设值
- (void)gc_setValue:(nullable id)value forKey:(NSString *)key;
//取值
- (nullable id)gc_valueForKey:(NSString *)key;@end@implementation NSObject (GCKVC)//设值
- (void)gc_setValue:(nullable id)value forKey:(NSString *)key{// 1、判断key 是否存在if (key == nil || key.length == 0) return;// 2、找setter方法,顺序是:setXXX、_setXXX、 setIsXXX// key 要大写NSString *Key = key.capitalizedString;// key 要大写NSString *setKey = [NSString stringWithFormat:@"set%@:", Key];NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key];NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key];if ([self gc_performSelectorWithMethodName:setKey value:value]) {NSLog(@"*************%@*************", setKey);return;}else if([self gc_performSelectorWithMethodName:_setKey value:value]){NSLog(@"*************%@*************", _setKey);return;}else if([self gc_performSelectorWithMethodName:setIsKey value:value]){NSLog(@"*************%@*************", setIsKey);return;}// 3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃if (![self.class accessInstanceVariablesDirectly]) {@throw [NSException exceptionWithName:@"GCUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];}// 4、间接访问变量赋值,顺序为:_key、_isKey、key、isKey// 4.1 定义一个收集实例变量的可变数组NSMutableArray *mArray = [self getIvarListName];// _<key> _is<Key> <key> is<Key>NSString *_key = [NSString stringWithFormat:@"_%@", key];NSString *_isKey = [NSString stringWithFormat:@"_is%@", Key];NSString *isKey = [NSString stringWithFormat:@"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、如果找不到则抛出异常@throw [NSException exceptionWithName:@"GCUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo: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];[mArray addObject:ivarName];}free(ivars);return mArray;
}@end

取值

取值的自定义代码如下,分为以下几步

  • 1、判断key非空
  • 2、查找相应方法,顺序是:get<Key>、 <key>、 countOf<Key>、 objectIn<Key>AtIndex
  • 3、判断是否能够直接赋值实例变量,即判断是否响应accessInstanceVariablesDirectly方法,间接访问实例变量,
    • 返回YES,继续下一步取值
    • 如果是NO,则崩溃
  • 4、间接访问实例变量,顺序是:_<key> _is<Key> <key> is<Key>
    • 4.1 定义一个收集实例变量的可变数组
    • 4.2 通过class_getInstanceVariable方法,获取相应的 ivar
    • 4.3 通过object_getIvar方法,返回相应的 ivar 的值
// 取值
- (nullable id)gc_valueForKey:(NSString *)key {// 1、判断非空if (key == nil || key.length == 0) {return nil;}// 2、找到相关方法:get<Key> <key> countOf<Key> objectIn<Key>AtIndex// key 要大写//capitalizedString 会将每个单词的首字母大写,其余小写。//例如:key = @"firstName" → capitalizedString 后为 @"FirstName" → 方法名 getFirstName。但是URL会变成UrlNSString *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:@(j)];[mArray addObject:objc];}return mArray;}}
#pragma clang diagnostic pop// 3、判断是否响应 `accessInstanceVariablesDirectly` 方法if (![self.class accessInstanceVariablesDirectly]) {@throw [NSException exceptionWithName:@"GCUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****", self] userInfo:nil];}// 4. 找相关实例变量进行取值,顺序为:_<key>、_is<Key>、<key>、is<Key>NSMutableArray *mArray = [self getIvarListName];NSString *_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);}return @"";
}

特性:无隐私访问

KVC 可访问私有属性(突破 @private/@protected 限制):

// Person.h
@interface Person : NSObject {@privateNSString *_secretCode; // 私有实例变量
}@property (nonatomic, copy) NSString *publicName; // 公开属性
@end// Person.m
@implementation Person
// 不暴露 _secretCode 的任何方法
@end

通过KVC强制访问私有属性

// 创建一个对象
Person *person = [[Person alloc] init];// 合法操作:通过属性访问公开字段
person.publicName = @"Alice";// 非法操作(编译器会报错):直接访问私有变量
// person->_secretCode = @"123"; //  编译错误:'secretCode' is private// 但通过 KVC 可以绕过限制
[person setValue:@"TOP_SECRET_123" forKey:@"secretCode"]; // 读取私有变量
NSString *stolenSecret = [person valueForKey:@"secretCode"];
NSLog(@"窃取的私有变量值: %@", stolenSecret); 
// 输出:窃取的私有变量值: TOP_SECRET_123
原理

KVC 查找顺序

  • 先尝试调用 setSecretCode:_setSecretCode: 方法(不存在)。
  • 然后直接访问实例变量 _secretCode(即使它是 @private)。

如果需要保护私有属性,可重写 accessInstanceVariablesDirectly 返回 NO

四、多元应用场景

1. 动态数据处理

(1)字典转模型
// 模型类
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end// 批量赋值
NSDictionary *data = @{@"name": @"Alice", @"age": @25};
Person *person = [[Person alloc] init];
[person setValuesForKeysWithDictionary:data]; // KVC 批量赋值
(2)模型转字典
NSDictionary *dict = [person dictionaryWithValuesForKeys:@[@"name", @"age"]]; // KVC 提取属性值

2. 系统控件定制

修改 UITextField 占位符颜色(私有属性 _placeholderLabel):

UITextField *textField = [[UITextField alloc] init];
UILabel *placeholderLabel = [textField valueForKey:@"_placeholderLabel"];
placeholderLabel.textColor = [UIColor lightGrayColor]; // 通过 KVC 修改私有属性

3. 高阶集合操作

在对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回 另一个集合(如:首字母大写):

NSArray *arr = @[@"hello", @"world"];
NSArray *capitalizedArr = [arr valueForKey:@"capitalizedString"]; // 结果:@[@"Hello", @"World"]

有如下Person类

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGFloat salary;
@end// 数据准备
NSArray *people = @[[Person personWithName:@"Alice" age:25 salary:5000],[Person personWithName:@"Bob" age:30 salary:8000],[Person personWithName:@"Charlie" age:28 salary:6000]
];

(1) 聚合运算

操作符作用示例
@count返回集合元素个数[people valueForKeyPath:@"@count"]3
@sum求和[people valueForKeyPath:@"@sum.age"]83
@avg平均值[people valueForKeyPath:@"@avg.salary"]6333.33
@max最大值[people valueForKeyPath:@"@max.age"]30
@min最小值[people valueForKeyPath:@"@min.salary"]5000

(2) 数组映射

操作符作用示例
@unionOfObjects提取属性组成新数组(不去重)[people valueForKeyPath:@"@unionOfObjects.name"]["Alice", "Bob", "Charlie"]
@distinctUnionOfObjects提取属性并去重若两人同名,结果会去重

(3) 嵌套集合操作

如果集合中嵌套集合(如 NSArray<NSArray *>):

NSArray *departments = @[@[person1, person2], @[person3, person4]
];
// 提取所有员工的 name(扁平化)
NSArray *allNames = [departments valueForKeyPath:@"@unionOfArrays.name"];

当然我们还可以通过自定义方法来实现自定义操作

@implementation Person// 自定义 KVC 集合操作符
- (id)valueForKeyPath:(NSString *)keyPath {if ([keyPath hasSuffix:@"@median"]) {NSArray *values = [self valueForKeyPath:[keyPath substringToIndex:keyPath.length - 7]];return [self medianOfArray:values];}return [super valueForKeyPath:keyPath];
}- (NSNumber *)medianOfArray:(NSArray *)numbers {// 计算中位数逻辑...
}@end// 使用
NSNumber *medianAge = [people valueForKeyPath:@"age@median"];
http://www.lryc.cn/news/597283.html

相关文章:

  • linxu CentOS 配置nginx
  • 【音视频学习】四、深入解析视频技术中的YUV数据存储方式:从原理到实践
  • 开源UI生态掘金:从Ant Design二次开发到行业专属组件的技术变现
  • 7月23日华为机考真题第二题-200分
  • 7月23日华为机考真题第一题100分
  • 关于原车一键启动升级手机控车的核心信息及注意事项
  • 将AI协作编程从“碰运气”的提示工程(Prompt Engineering)提升到“可预期”的上下文工程(Context Engineering)
  • 驯服AI的“魔法咒语”:Prompt提示词工程使用教程
  • [特殊字符] 从数据库无法访问到成功修复崩溃表:一次 MySQL 故障排查实录
  • 显微科研中的关键选择:不同显微镜相机技术特性与应用适配性全面解析
  • SpringBoot Stream实战指南
  • Django学习之旅--第13课:Django模型关系进阶与查询优化实战
  • 电科金仓推出AI融合数据库,开启国产数据库新时代
  • 深入理解 Java Builder 设计模式:解决构造函数爆炸问题
  • Java SE:类与对象的认识
  • 编程语言Java——核心技术篇(二)类的高级特性
  • Python 程序设计讲义(9):Python 的基本数据类型——复数
  • LeetCode|Day23|326. 3 的幂|Python刷题笔记
  • Flask框架全面详解
  • Element中ElMessageBox弹框内容及按钮样式自定义
  • 服务器版本信息泄露-iis返回包暴露服务器版本信息
  • [Linux入门] Linux 文件系统与日志分析入门指南
  • Linux中scp命令传输文件到服务器报错
  • (Arxiv-2025)利用 MetaQueries 实现模态间迁移
  • 在 Ubuntu 上将 Docker 降级到版本 25.0.5 (二) 降低版本,涉及兼容性问题
  • 欧盟网络安全标准草案EN 18031详解
  • 我用EV-21569-SOM评估来开发ADSP-21569(十三)-SigmaStudio Plus做开发(4)
  • sqlsuger 子表获取主表中的一个字段的写法
  • 进程间通信之-----零拷贝
  • AI替代人工:浪潮中的沉浮与觉醒