KVC:全名Key Value Coding,通常我们将它叫作“键值编码”,官网有详细的解释。它可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值,而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。那么它的原理究竟是什么样子的呢?
一: 基本使用
常见API
- (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
复制代码
其他API
// 默认为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;
复制代码
示例代码:
#import <Foundation/Foundation.h>
#import "WYStudent.h"
typedef struct {
float x, y, z;
} ThreeFloats;
?
@interface WYPerson : NSObject{
@public
NSString *myName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, strong) NSMutableArray *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic) ThreeFloats threeFloats;
@property (nonatomic, strong) WYStudent *student;
@end
?
@interface WYStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int length;
@property (nonatomic, strong) NSMutableArray *penArr;
@end
复制代码
1 访问对象属性
setValue
和valueForKey
WYPerson *person = [[WYPerson alloc] init];
[person setValue:@"啊云" forKey:@"name"];
NSLog(@"person 的姓名为: %@", [person valueForKey:@"name"]);
打印结果:
person 的姓名为: 啊云
复制代码
valueForKeyPath:
和 setValue:ForKeyPath
WYStudent *student = [WYStudent alloc];
[person setValue:@"Swift" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
打印结果:
person 的姓名为: Swift
复制代码
2 访问集合属性
第一种方法:直接用一个新数组进行赋值
NSArray *array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
打印结果:100,2,3
复制代码
第二种方法:取出数组以可变数组形式保存,再修改(推荐使用这种方法)
WYPerson *person = [[WYPerson alloc] init];
person.array = @[@"1",@"2",@"3"];
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
[mArray addObject:@(100)];
NSLog(@"%@",[person valueForKey:@"array"]);
打印结果:200,2,3,100
复制代码
mutableArrayValueForKey:
这个方法会通过传入的 key
返回对应属性的一个可变数组的代理对象,对于集合对象,苹果为我们提供了更友好的方法。
-
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
- These return a proxy object that behaves like an NSMutableArray object.
- 【译】返回的代理对象表现为一个
NSMutableArray
对象
-
mutableSetValueForKey:
和mutableSetValueForKeyPath:
- These return a proxy object that behaves like an NSMutableSet object.
- 【译】返回的代理对象表现为一个
NSMutableSet
对象
-
mutableOrderedSetValueForKey:
andmutableOrderedSetValueForKeyPath:
-
These return a proxy object that behaves like an NSMutableOrderedSet object.
-
【译】返回的代理对象表现为一个
NSMutableOrderedSet
对象
-
3 访问字典
setValuesForKeysWithDictionary:
用来修改Model中对应key的属性(字典转模型)
dictionaryWithValuesForKeys:
输入一组key,返回这组key对应的属性,再组成一个字典
举个?
NSDictionary* dict = @{
@"name":@"Cooci",
@"nick":@"KC",
@"subject":@"iOS",
@"age":@18,
@"length":@180
};
WYStudent *p = [[WYStudent alloc] init];
// 字典转模型
[p setValuesForKeysWithDictionary:dict];
NSLog(@"%@",p);
// 键数组转模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *dic = [p dictionaryWithValuesForKeys:array]
NSLog(@"%@",dic);
? 打印结果
<WYStudent: 0x600003636eb0>
age = 18;
name = Cooci;
**}**
复制代码
4 集合运算符
集合运算符可以分为三大类:
-
聚合操作符
@avg
: 返回操作对象指定属性的平均值@count
: 返回操作对象指定属性的个数@max
: 返回操作对象指定属性的最大值@min
: 返回操作对象指定属性的最小值@sum
: 返回操作对象指定属性值之和
-
数组操作符
@distinctUnionOfObjects
: 返回操作对象指定属性的集合–去重@unionOfObjects
: 返回操作对象指定属性的集合
-
嵌套操作符
@distinctUnionOfArrays
: 返回操作对象(嵌套集合)指定属性的集合–去重,返回的是NSArray
@unionOfArrays
: 返回操作对象(集合)指定属性的集合@distinctUnionOfSets
: 返回操作对象(嵌套集合)指定属性的集合–去重,返回的是NSSet
聚合操作符?
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
WYStudent *p = [WYStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"])
/// 平均身高
float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
NSLog(@"%f", avg);
int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
NSLog(@"%d", count);
int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
NSLog(@"%d", sum);
int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
NSLog(@"%d", max)
int min = [[personArray valueForKeyPath:@"@min.length"] intValue]
NSLog(@"%d", min);
复制代码
数组操作符?
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
WYStudent *p = [WYStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"]);
// 返回操作对象指定属性的集合
NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.length"];
NSLog(@"arr1 = %@", arr1);
// 返回操作对象指定属性的集合 -- 去重
NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];
NSLog(@"arr2 = %@", arr2);
复制代码
5 访问非对象属性
非对象属性分为两类,一类是基本数据类型,一类是结构体(struct)
对于基本数据类型,常用的基本数据类型需要在设置属性的时候包装成 NSNumber
类型,然后在读取值的时候使用各自对应的读取方法,如 double
类型的标量读取的时候使用 doubleValue
对于结构体类型来说,需要转化为NSValue
// 结构体
// 设值
ThreeFloats floats = {1.,2.,3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
// 取值
NSValue *value1 = [person valueForKey:@"threeFloats"];
NSLog(@"%@",value1);
ThreeFloats th;
[value1 getValue:&th];
NSLog(@"%f-%f-%f",th.x,th.y,th.z);
复制代码
6 属性验证
该功能是通过validateValue:forKey:error:
来实现
需要注意的是这个方法系统不会自己调用,需要我们手动实现
@implementation WYPerson
- (BOOL)validateName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{
NSString* name = *value;
name = name.capitalizedString;
if ([name isEqualToString:@"Not-name"]) {
return NO;
}
return YES;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
WYPerson *person = [[WYPerson alloc]init];
NSError* error;
NSString *value = @"Not-name"; // result 为 NO
//NSString *value = @"BookName"; // result 为 YES
BOOL result = [person validateValue:&value forKey:@"name" error:&error];
if (result) {
NSLog(@"OK");
} else {
NSLog(@"NO");
}
}
复制代码
7 异常处理
key不存在
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"您设置的key:[%@]不存在", key);
NSLog(@"您设置的value为:[%@]", value);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"您访问的key:[%@]不存在", key);
return nil;
}
复制代码
value为nil
- (void)setNilValueForKey:(NSString *)key {
//对不能接受nil的属性进行处理
if ([key isEqualToString:@"price"]) {
//对应你具体的业务来处理
price = 0;
}else {
[super setNilValueForKey:key];
}
}
复制代码
二: 底层原理分析
1 设值原理
setValue:forKey:
方法默认实现会在调用者传入 key
和 value
(如果是非对象类型,则指的是解包之后的值) 之后会在对象中按下列的步骤进行模式搜索:
-
1.以
set<Key>:
,_set<Key>
的顺序在对象中查找是否有这样的方法,如果找到了,则把属性值传给方法来完成属性值的设置。 -
2.判断类方法
accessInstanceVariablesDirectly
结果- 如果返回
YES
,则以_<key>
,_is<Key>
,<key>
,is<Key>
的顺序查找成员变量,如果找到了,则把属性值传给方法来完成属性值的设置。 - 如果返回
NO
,跳转到第 3 步
- 如果返回
-
3.调用
setValue:forUndefinedKey:
。 默认情况下,这会引发一个异常,但是NSObject
的子类可以提供特定于key
的行为。
2 取值原理
valueForKey:
方法会在调用者传入 key
之后会在对象中按下列的步骤进行模式搜索:
-
1.以
get<Key>
,<key>
,is<Key>
以及_<key>
的顺序查找对象中是否有对应的方法。- 如果找到了,将方法返回值带上跳转到第 5 步
- 如果没有找到,跳转到第 2 步
-
2.查找是否有
countOf<Key>
和objectIn<Key>AtIndex:
方法(对应于NSArray
类定义的原始方法)以及<key>AtIndexes:
方法(对应于NSArray
方法objectsAtIndexes:
)- 如果找到其中的第一个(
countOf<Key>
),再找到其他两个中的至少一个,则创建一个响应所有NSArray
方法的代理集合对象,并返回该对象。(翻译过来就是要么是countOf<Key>
+objectIn<Key>AtIndex:
,要么是countOf<Key>
+<key>AtIndexes:
,要么是countOf<Key>
+objectIn<Key>AtIndex:
+<key>AtIndexes:
) - 如果没有找到,跳转到第 3 步
- 如果找到其中的第一个(
-
3.查找名为
countOf<Key>
,enumeratorOf<Key>
和memberOf<Key>
这三个方法(对应于NSSet类定义的原始方法)- 如果找到这三个方法,则创建一个响应所有
NSSet
方法的代理集合对象,并返回该对象 - 如果没有找到,跳转到第 4 步
- 如果找到这三个方法,则创建一个响应所有
-
4.判断类方法
accessInstanceVariablesDirectly
结果- 如果返回
YES
,则以_<key>
,_is<Key>
,<key>
,is<Key>
的顺序查找成员变量,如果找到了,将成员变量带上跳转到第 5 步,如果没有找到则跳转到第 6 步 - 如果返回
NO
,跳转到第 6 步
- 如果返回
-
5.判断取出的属性值
- 如果属性值是对象,直接返回
- 如果属性值不是对象,但是可以转化为
NSNumber
类型,则将属性值转化为NSNumber
类型返回 - 如果属性值不是对象,也不能转化为
NSNumber
类型,则将属性值转化为NSValue
类型返回
-
6.调用
valueForUndefinedKey:
。 默认情况下,这会引发一个异常,但是NSObject
的子类可以提供特定于key
的行为。
三:应用
动态的设置和取值
利用KVC动态的取值和设值是最基本的用途
访问私有属性
对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的。
字典转模型
和runtime配合,使用setValuesForKeysWithDictionary
实现字典转模型。
访问控件的内部属性
举个?,使用KVC修改textfield占位文字颜色和字号
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[textField setValue:[UIFont boldSystemFontOfSize:16] forKeyPath:@"_placeholderLabel.font"];
复制代码
消息传递
举个?
NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
NSArray *lenStr= [array valueForKeyPath:@"length"]; NSLog(@"%@",lenStr);// 消息从array传递给了string
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"%@",lowStr);
? 打印结果:
beijing,shanghai,guangzhou,shenzhen
复制代码
以上例子是把数组内所有元素首字母变成小写,valueForKey
会将消息传递给每个元素,而不是容器本身。