前言
“这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战”
资源准备
KCV的简介
定义
KVC
的全称是Key-Value Coding
(键值编码),是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问,这种间接访问机制补充了实例变量及其关联的访问方法所提供的直接访问,可以通过字符串来访问一个对象的成员变量或其关联的存取方法。当一个对象符合键值编码时,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数来寻址。
在Objective-C
中,KVC
相当于NSObject
的分类。查看setValueForKey
方法,是在Foundation
里面,而Foundation
框架是不开源
的,只能在苹果官方文档查找。再看看 API
,是Foundation
框架的NSKeyValueCoding
文件:
本质上是对NSObject
、NSArray
、NSDictionary
、NSMutableDictionary
、NSOrderedSet
、NSSet
等,增加了NSKeyValueCoding
分类,让它们具备Key-Value Coding
的能力。
API
介绍
- 通过
Key
读取和存储:
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
复制代码
- 通过
keyPath
读取和存储:
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
复制代码
- 其他
API
:
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员
+ (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;
复制代码
API
使用
对象属性
- 属性:这些是简单的值,例如标量、字符串或布尔值。值对象
NSNumber
和其他不可变类型NSColor
也被视为属性; - 一对一的关系:这些是具有自己属性的可变对象。一个对象的属性可以在不改变对象本身的情况下改变;
- 对多关系:这些是集合对象。通常使用
NSArray
或的实例NSSet
来保存此类集合,也可能是自定义集合类型;
通过Key
查询
LGPerson *person = [[LGPerson alloc] init];
// 通过`setter`方法为`name`属性赋值:
person.name = @"LG_Cooci";
person.age = 20;
person->myName = @"cooci";
// 1:Key-Value Coding (KVC) : 基本类型 - 看底层原理
// 非正式协议 - 间接访问
[person setValue:@"KC" forKey:@"name"];
复制代码
通过keyPath
查询
LGStudent *student = [LGStudent alloc];
student.subject = @"五谷丰登";
person.student = student;
[person setValue:@"Swift" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
复制代码
获得的打印结果:Swift
;通过kvc
再结合路径student.subject
进行了替换。
集合属性
对外的 api
接口:
- 对
NSMutableArray
:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; // 通过 key
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath; // 通过 keyPath
复制代码
- 对
NSMutableSet
:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key; // 通过 key
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath; // 通过 keyPath
复制代码
- 对
NSMutableOrderedSet
:
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)); // 通过 key
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)); // 通过 keyPath
复制代码
可以通过具体代码实现来看下:
- 数组取值
LGStudent *p = [LGStudent new];
p.penArr = [NSMutableArray arrayWithObjects:@"pen0", @"pen1", @"pen2", @"pen3", nil];
NSArray *arr = [p valueForKey:@"penArr"];
NSLog(@"pens = %@", arr); //打印arr数组
NSEnumerator *enumerator = [arr objectEnumerator];
NSString* str = nil;
while (str = [enumerator nextObject]) {
NSLog(@"%@", str); //遍历打印
}
复制代码
打印arr数组结果:
遍历打印结果:
- 修改数组元素
person.array = @[@"1",@"2",@"3"];
// 第一种:创建新的数组,KVC赋值就
NSArray *array = [person valueForKey:@"array"];
array = @[@"10",@"8",@"9"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]); // 修改前打印
// 第二种:使用mutableArrayValueForKey:
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"100";
NSLog(@"%@",[person valueForKey:@"array"]); // 修改后打印
复制代码
修改前打印:
修改后打印:
- 模型字典转换
NSDictionary* dict = @{
@"name":@"Cooci",
@"nick":@"KC",
@"subject":@"iOS",
@"age":@18,
@"length":@190
};
LGStudent *s = [[LGStudent alloc] init];
// 字典转模型
[s setValuesForKeysWithDictionary:dict];
NSLog(@"模型:%@",s);
// 键数组转模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *dic = [s dictionaryWithValuesForKeys:array];
NSLog(@"字典:%@",dic);
复制代码
字典转模型转化:
键数组转模型到字典打印:
KVC
消息传递
NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
NSArray *lenStr= [array valueForKeyPath:@"length"];
// 消息从array传递给了string
NSLog(@"%@",lenStr);
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"]; // 关键字 lowercaseString
NSLog(@"%@",lowStr);
复制代码
打印字符串长度:
打印字符串:
- 集合运算符
集合操作符有三种基本类型:
-
1
、聚合操作符:以某种方式合并集合的对象,并返回一个通常与在正确键路径中命名的属性的数据类型匹配的对象。@count
操作符是个例外,它不接受正确的键路径,并且总是返回一个NSNumber
实例; -
2
、数组操作符:返回一个NSArray
实例,该实例包含命名集合中持有的对象的一些子集; -
3
、嵌套操作符:处理包含其他集合的集合,并根据操作符返回一个NSArray
或NSSet
实例,以某种方式组合嵌套集合的对象;
接下来,通过案例使用来展示下。
- 聚合操作符
打印结果,分别是:数组的 length
、平均值、数组个数、总值、最大值、最小值:
- 数组操作符
打印结果:
- 嵌套操作符
打印结果:
排重:(排重 ---- arr = 185, 183, 179, 177 )
不排重:(不排重 ---- arr1 = 177, 177, 177, 183, 185, 177, 183, 179, 179, 179, 179, 179 )
复制代码
访问非对象属性
- 默认键值编码实现使用
NSNumber
实例包装的标量类型:
- 默认存取用于包装和展开常见
NSPoint
、NSRange
、NSRect
、和NSSize
结构:
- 结构类型,可以包装在一个
NSValue
对象中:
结构体:
调用:
打印结果:
属性的验证
通过特定于属性的验证,方法如下:
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
复制代码
可以得到三种有可能的结果:
-
1
、如果是属性是有效的,并不改变值或没报错误,返回YES
; -
2
、如果值是无效,但选择不更改它。在这种情况下,该方法返回NO
。并且返回错误outError
; -
3
、如果值无效,但会创建一个新的有效对象作为替换。在这种情况下,该方法返回YES
。同时保持错误对象不变。在返回之前,该方法修改值引用以指向新的值对象。当它进行修改时,该方法总是创建一个新对象,而不是修改旧对象,即使值对象是可变的;
KVC
的存储过程
通过调用setValue:forKey:
实现:
-
①
查找是否存在以下三种setter
方法,按顺序查找名为:set<Key>:
→_set<Key>
→setIs<Key>
访问器顺序查找,如果找到就调用它;key
是指成员变量名,首字母大小写需要符合KVC
的命名规范;- 如果存在任意一种
setter
方法,都能直接设置属性的value
,传入进来;
-
如果没有找到这些
setter
方法,进入②
步骤:KVC
机制会检查+ (BOOL)accessInstanceVariablesDirectly
方法有没有返回YES
:- 返回
YES
,查找间接访问的实例变量进行赋值,查找顺序:_<key>
→_is<Key>
→<key>
→is<Key>
,如果找到其中任意一个实例变量,可对其赋值 - 如果都未找到,就返回
NO
,那么将进入步骤③
;
- 返回
-
③
如果setter
方法或实例变量都没有找到,系统会调用该对象的setValue:forUndefinedKey:
方法,默认抛出NSUndefinedKeyException
类型的异常;
通过案例展示:
#import <Foundation/Foundation.h>
@interface LGPerson : NSObject{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
@implementation LGPerson
#pragma mark - 关闭/开启实例变量
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
@end
复制代码
setter
方法
- (void)setName:(NSString *)name {
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name {
NSLog(@"%s - %@",__func__,name);
}
- (void)setIsName:(NSString *)name {
NSLog(@"%s - %@",__func__,name);
}
// 没有调用
- (void)_setIsName:(NSString *)name {
NSLog(@"%s - %@",__func__,name);
}
复制代码
示意图:
KVC
的读取过程
取值,是调用valueForKey:
进行获取的,其流程是:
-
①
、首先是查找getter
方法,查找顺序为:get<Key>
–><key>
–>is<Key>
–>_<key>
。如果找到,根据搜索到的属性值的类型,返回不同的结果:- 如果是对象,则直接返回结果;
- 如果是
BOOL
或者Int
等值类型, 会将其包装成一个NSNumber
对象,将其存储在NSNumber
实例中并返回它;
-
②
、如果未找到,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
的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC
的标准命名方法,包括方法签名。
③
、如果还是未找到,那么会同时查找以下几种方法,countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
。
如果上面三个方法都找到,就会返回一个可以响应所有NSSet
方法的集合代理,此代理对象随后将其收到的所有NSSet
消息转换为countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
消息的某种组合,进行调用。
-
④
、如果未找到,会再接着查找accessInstanceVariablesDirectly
方法的返回值,如果返回YES
,依次搜索_<key>
、_is<Key>
、<key>
或is<Key>
的实例变量(不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱),找到实例变量,直接获取实例变量的值; -
⑤
、如果返回NO
,系统调用该对象的valueForUndefinedKey:
方法,默认抛出NSUndefinedKeyException
类型的异常。
下面通过案例来看看:
先查找 getter
方法
//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name {
return NSStringFromSelector(_cmd);
}
- (NSString *)isName {
return NSStringFromSelector(_cmd);
}
- (NSString *)_name {
return NSStringFromSelector(_cmd);
}
复制代码
再查找集合类型
//MARK: - 集合类型的走
// 个数
- (NSUInteger)countOfPens {
NSLog(@"%s",__func__);
return [self.arr count];
}
// 获取值
- (id)objectInPensAtIndex:(NSUInteger)index {
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"pens %lu", index];
}
复制代码
接着再查找NSSet
类型
//MARK: - set
// 个数
- (NSUInteger)countOfBooks {
NSLog(@"%s",__func__);
return [self.set count];
}
// 是否包含这个成员对象
- (id)memberOfBooks:(id)object {
NSLog(@"%s",__func__);
return [self.set containsObject:object] ? object : nil;
}
// 迭代器
- (id)enumeratorOfBooks {
// objectEnumerator
NSLog(@"来了 迭代编译");
return [self.arr reverseObjectEnumerator];
}
复制代码
查找实例变量
#import <Foundation/Foundation.h>
@interface LGPerson : NSObject {
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
@implementation LGPerson
#pragma mark - 关闭/开启实例变量
+ (BOOL)accessInstanceVariablesDirectly {
return YES;
}
@end
复制代码
图解:
自定义 KVC
KVC
存储
- 1、判断什么
key
; - 2、
setter set<Key>: or _set<Key>
; - 3、判断是否响应
accessInstanceVariablesDirectly
响应返回YES
,不响应返回NO
,就直接奔溃;判断是否能够直接赋值实例变量; - 4、间接变量,获取
ivar
-> 遍历containsObjct
- 4.1 定义一个收集实例变量的可变数组;
- 4.2 获取相应的
ivar
; - 4.3 对相应的
ivar
设置值;
- 5、如果找不到相关实例;
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
// KVC 自定义
// 1: 判断什么 key
if (key == nil || key.length == 0) {
return;
}
// 2: setter set<Key>: or _set<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 lg_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
// 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException"
reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self]
userInfo:nil];
}
// 4: 间接变量
// 获取 ivar -> 遍历 containsObjct -
// 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 获取相应的 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:@"LGUnknownKeyException"
reason:[NSString
stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)]
userInfo:nil];
}
复制代码
KVC
取值
- 1、筛选
key
判断非空; - 2、找到相关方法
get<Key> <key> countOf<Key> objectIn<Key>AtIndex
; - 3、判断是否能够直接赋值实例变量;
- 4、找相关实例变量进行赋值;
- 4.1、定义一个收集实例变量的可变数组
// 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 pop
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException"
reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self]
userInfo:nil];
}
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// _name -> _isName -> name -> isName
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 @"";
复制代码
相关方法
#pragma mark - 相关方法
- (BOOL)lg_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 pop
return 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;
}
复制代码