「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,_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:-> _set -> setIs
- 如果
有其中任意一个
setter方法,则直接设置属性的value
(注意:key是指成员变量名
,首字符大小写需要符合KVC的命名规范) - 如果都
没有
,则进入【第二步】
- 如果
-
【第二步】:如果没有第一步中的三个简单的setter方法,则查找
accessInstanceVariablesDirectly
是否返回YES
-
如果返回
YES
,则查找间接访问的实例变量进行赋值,查找顺序为:
_ -> _is -> -> is
- 如果找到其中任意一个实例变量,则赋值
- 如果都没有,则进入【第三步】
-
如果返回
NO
,则进入【第三步】
-
-
【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的
setValue:forUndefinedKey:
方法,默认抛出NSUndefinedKeyException
类型的异常
综上所述,KVC通过 setValue:forKey: 方法设值
的流程以设置LGPerson的对象person的属性name为例,如下图所示
KVC 取值 底层原理
同样的,我们可以通过官方文档分析KVC取值的底层原理
当调用valueForKey:
时,其底层的执行流程如下
-
【第一步】首先查找getter方法,按照
get -> -> is -> _
的方法顺序查找,
- 如果
找到
,则进入【第五步】 - 如果
没有找到
,则进入【第二步】
- 如果
-
【第二步】如果第一步中的getter方法没有找到,KVC会查找.
countOf 和objectIn AtIndex :和 AtIndexes
- 如果找到
countOf
和其他两个中的一个,则会创建一个响应所有NSArray
方法的集合代理对象
,并返回该对象,即NSKeyValueArray
,是NSArray
的子类
。代理对象随后将接收到的所有NSArray消息转换为countOf,objectIn AtIndex:和AtIndexes:
消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get:range:
之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。) - 如果没有找到这三个访问数组的,请继续进入【第三步】
- 如果找到
-
【第三步】如果没有找到上面的几种方法,则会同时查找
countOf ,enumeratorOf和memberOf
这三个方法- 如果这三个方法都找到,则会创建一个响应
所有NSSet方法的集合代理对象
,并返回该对象,此代理对象随后将其收到的所有NSSet
消息转换为countOf,enumeratorOf和memberOf:
消息的某种组合,用于创建它的对象 - 如果还是没有找到,则进入【第四步】
- 如果这三个方法都找到,则会创建一个响应
-
【第四步】如果还没有找到,检查类方法
InstanceVariablesDirectly
是否YES,依次搜索_,_is,或is
的实例变量- 如果搜到,
直接获取实例变量的值
,进入【第五步】
- 如果搜到,
-
【第五步】根据搜索到的属性值的类型,返回不同的结果
- 如果是
对象指针
,则直接返回结果 - 如果是
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]; // _ _is is NSString *_key = [NSString stringWithFormat:@\"_%@\", key]; NSString *_isKey = [NSString stringWithFormat:@\"_is%@\", Key]; NSString *isKey = [NSString stringWithFormat:@\"is%@\", Key]; if ([mArray containsObject:_key]) { // 4.2 获取相应的 ivar Ivar 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、 、 countOf、 objectInAtIndex
- 3、判断是否能够直接赋值实例变量,即判断是否响应
accessInstanceVariablesDirectly
方法,间接访问实例变量,- 返回
YES
,继续下一步取值 - 如果是
NO
,则崩溃
- 返回
- 4、间接访问实例变量,顺序是
:_ _is is
- 4.1 定义一个收集实例变量的
可变数组
- 4.2 通过
class_getInstanceVariable
方法,获取相应的 ivar - 4.3 通过
object_getIvar
方法,返回相应的 ivar 的值
- 4.1 定义一个收集实例变量的
// 取值- (nullable id)gc_valueForKey:(NSString *)key { // 1、判断非空 if (key == nil || key.length == 0) { return nil; } // 2、找到相关方法:get countOf objectInAtIndex // key 要大写 //capitalizedString 会将每个单词的首字母大写,其余小写。//例如:key = @\"firstName\" → capitalizedString 后为 @\"FirstName\" → 方法名 getFirstName。但是URL会变成Url 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:@(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. 找相关实例变量进行取值,顺序为:_、_is、、is 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 { @private NSString *_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 *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\"];