KVC
相关类及方法 ?
1. 定义 : KVC(Key-value coding)键值编码
从定义上很明显了解到KVC是基于key - value 的一些操作
而最容易想到的是NSDictionary 操作了
NSDictionary *dic = [[NSDictionary alloc]init];
[dic setValue:@"23" forKey:@"age"];
[dic objectForKey:@"age"];
2. 接着, 我们进入 NSDictionary.h 系统文件
发现NSDictionary实例中并没有实现setValue:forKey: 和 objectForKey: 方法
(也不在NSObject中)
接着我们之间点击上面[dic setValue:@"23" forKey:@"age"]; 跳转进入系统方法
:
我们进入到了系统类NSKeyValueCoding.h
3. NSKeyValueCoding.h
文件的声明 : @interface NSObject(NSKeyValueCoding)
发现有熟悉的一些方法 :
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
等等还有其他方法,下面再解释
发现KVC 是基于NSObject 扩展功能
KVO是基于KVC基础上
同样也基于其他类扩展一些方法, 比如 :
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
因此, 任何NSObject (或子类)对象都支持KVC方法
那么问题来了, 对象forKey对应属性可能不存在 ?
这就涉及到runtime了
KVC 动态性
说明 :
KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通
过Key名(字符串)直接访问对象的属性,或者给对象的属性赋值。而不需要调用明
确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编
译时确定
因此方法 [dic objectForKey:@"age"]; 编译时不管dic 是否存在属性age, 而是到
运行的时候才会正常,或者抛出错误
既然是在运行时动态地访问和修改, 那么必定涉及消息转发和异常处理了
(runtime)
底层如何实现? 如何消息转发 ?
注意 : KVC 可以显式或者隐式获取或者修改一些属性值 , 包括
自定义和系统私有属性
但是前提条件是字符串key对应的属性一定要存在, 编译不会对字符串key进行校
验,运行时失败会导致异常
KVC 底层
(1 设置值). 当调用[user setValue:@“abc” forKey:@”name“]; 的代码时, 下面看
看怎么执行的 :
设值步骤 :
1.1. 首先调用 setName: 方法 ,
如果找不到就执行下一步
说明, 声明一个属性就自动生成set和get方法 :
@property(nonatomic,strong)NSString *name;
没有声明上面属性就没有setName方法
1.2. 检查+ (BOOL)accessInstanceVariablesDirectly
方法返回值
NO :
1.3. 执行setValue:forUndefinedKey:方法
A. 可以重写这个方法处理异常
B. 没有处理时,直接抛异常,返回null
YES :
1.4. 按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作
1.5. 如果还是没有, 再执行setValue:forUndefinedKey:方法
补充变量的几种声明方式 :
@interface Test: NSObject {
NSString *_name; //方式1
NSString *_isName; //方式2
NSString *name; //方式3
NSString *isName; //方式4
}
...
@property(nonatomic,copy)NSString *name; //方式0
(2 取值) 当调用valueForKey:@”name“的代码时
取值步骤 :
当调用valueForKey:取值时:
1. 首先按照 getName , name , isName 顺序方法查找getter方法
如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象
-(NSString *)getName {
...
}
-(NSString *)name {
...
}
-(NSString *)isName {
...
}
2. 如果上面的getter没有找到,KVC则会查找NSArray的方法 :
先是 : countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方
法 , get<Key>:range:
没有再找 : countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的
方法
3. 如果NSArray找不到, 就响应NSSet方法
countOf<Key>,enumeratorOf<Key>,memberOf<Key>
4. 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
如果返回YES(默认行为):
会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名
返回NO的话
直接调用valueForUndefinedKey:方法,默认是抛出异常
(3) KVC使用keyPath
需求 : 当目标是对象的属性的属性 时(深层关系) 时, 该如何处理 ?
KVC提供了一个解决方案,那就是键路径keyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过
KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过
KeyPath来设值
使用 :
比如设置: user.type.name
[user setValue:@“student” forKeyPath:@"type.name"];
(4) KVC 异常处理 :
编译无法对字符串进行语法校验, 这也是导致KVC使用容易出现异常的根本原因 :
1. KVC处理nil异常
设置空值
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"不能将%@设成nil", key);
}
2. KVC处理UndefinedKey异常
属性或者方法不存在
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@",key);
return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出现异常,该key不存在%@", key);
}
(5) KVC键值验证
既然编译无法检验语法, 那么就需要 提前手动验证是否能成功
KVC提供了验证Key对应的Value是否可用的方法:
使用test例子:
@autoreleasepool {
//Test生成对象
Test *test = [[Test alloc] init];
//通过KVC设值test的age
NSNumber *age = @10;
NSError* error;
NSString *key = @"age";
BOOL isValid = [test validateValue:&age forKey:key error:&error];
if (isValid) {
NSLog(@"可以匹配再进行设置值 :”);
[test setValue:age forKey:key];
}
else {
NSLog(@"键值不匹配");
}
//通过KVC取值age打印
NSLog(@"test的年龄是%@", [test valueForKey:@"age"]);
}
(6) KVC属性类型要求 :
重要 : KVC 处理的值必须是NSObject 对象或其子类
NSString
NSValue (注意)
NSNumber (注意)
其它常见等等
1. KVC 总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值
会封装成NSNumber或者NSValue对象
需要转 NSNumber :
BOOL
NSInteger
int , unsignedint
long , unsignedlong , longlong , unsignedlonglong
float , double ...
char, unsignedchar
short, unsignedshort
需要转 NSValue :
CGPoint , CGSize , CGRect , CGAffineTransform
UIEdgeInsets, UIOffset
实例 :
1. 比如age是NSInteger对象(转NSNumber):
//设置值
[obj setValue:@(6) forKey:@"age"];
//错误使用返回
NSInteger age = [obj valueForKey:@"age"];
//正确使用返回
NSNumber *age = [obj valueForKey:@"age"];
2. BOOL值(转NSNumber)
//设置
[obj setValue:@(YES) forKey:@"girl"];
//正确使用, 转换bool值
BOOL age = [[obj valueForKey:@"girl"] boolValue];
3. CGSize转NSValue
+ (NSValue*)valueWithCGSize:(CGSize)size;
(7) KVC处理集合 :
KVC除了能获取单个对象属性, 还能对列表所有对象的属性进行简单或复杂运算
1. 简单集合运算符共有@avg, @count , @max , @min ,@sum5种
2. 对象运算符@distinctUnionOfObjects , @unionOfObjects
实例 :
简单运算
//一个班级学生
NSArray* arrPerson = @[person1,person2,person3,person4];
//平均年龄
NSNumber* avg = [arrPerson valueForKeyPath:@"@avg.age"];
//学生数
NSNumber* count = [arrPerson valueForKeyPath:@"@count"];
//最小年龄
NSNumber* min = [arrPerson valueForKeyPath:@"@min.age"];
//最大年龄
NSNumber* max = [arrPerson valueForKeyPath:@"@max.age"];
//年龄总值
NSNumber* sum = [arrPerson valueForKeyPath:@"@sum.age"];
对象运算符
//不同年龄集合(去重)
NSArray* arrAges = [arrPerson
valueForKeyPath:@"@distinctUnionOfObjects.age"];
//不同年龄集合(不去重)
NSArray* arrUnionAges = [arrPerson
valueForKeyPath:@"@unionOfObjects.age"];
高阶消息传递 :
NSArray* arrStr = @[@"abc",@"223",@"hjj"];
//返回所有字符串新数组
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
//返回字符串长度number数组
NSArray* arrCapStrLength = [arrStr
valueForKeyPath:@"capitalizedString.length"];
(8) KVC 处理字典 :
常见的就是字典解析了
1. 模型转字典
//把对应key所有的属性全部取出来
NSDictionary* dict = [model dictionaryWithValuesForKeys:arr];
2. 字典转模型
//用key Value来修改Model的属性
NSDictionary* dict = @{@"name":@"aa" ... }
[model setValuesForKeysWithDictionary: dict];
(9) 访问和修改私有变量 :
在xcode11之前 我么经常会使用KVC 访问和修改私有变量
比如导航,tabbar修改,背景图等等
比如UITextField中的placeHolderText
但是 : 在Xcode 11 之后开始限制了,部分控件 访问和修改私有变量 会直接崩溃.