iOS KVC
Posted xiaoxiaobukuang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS KVC相关的知识,希望对你有一定的参考价值。
一、KVC简介
KVC
全称是Key Value Coding
(键值编码),是一个基于NSKeyValueCoding
非正式协议实现的机制,他可以直接通过Key
值对对象的属性存取操作,而不需通过明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定。KVC
提供了一种间接访问属性方法或成员变量的机制,可以通过字符串来访问对象的属性方法或成员变量;- 在实现了访问器方法的类中,使用点语法和
KVC
访问对象其实差别不大,二者可以任意混用(因为KVC
会首先搜索访问器方法,见下文)。但是没有访问器的类种,点语法无法使用,这时KVC
就有优势了。 KVC
和KVO
都是基于OC
的动态特性和Runtime
机制的。
二、KVC通用的访问方法
1、通用的访问方法
- getter方法
- (nullable id)valueForKey:(NSString *)key;
- setter方法
- (void)setValue:(nullable id)value forKey:(NSString *)key;
2、衍生的KeyPath方法,用来进行深层访问(Key使用点语法),也可以单层访问
keyPath
的setter
方法:
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
keyPath
的getter
方法:
- (nullable id)valueForKeyPath:(NSString *)keyPath;
3、keyPath
除了对当前对象的属性进行赋值外,还可以对其更“深层”的对象进行访问。
keypath
可以访问到array
数组中所有存储的对象的属性,前提是对象元素要有同样名称的属性。
NSMutableArray *mArray = [NSMutableArray array];
for (int i = 0; i < 10; i ++)
Person *person = [[Person alloc]init];
person.name = [NSString stringWithFormat:@"名字%d",i];
person.sex = i%2;
person.age = [NSNumber numberWithInt:i];
[mArray addObject:person];
Address *address = [[Address alloc]init];
address.name = @"名字11";
[mArray addObject:address];
NSArray *nameArray = [mArray valueForKeyPath:@"name"];
三、KVC的多值操作
1、批量取值操作
KVC
还有更强大的功能,可以根据给定的一组key
,获取到一组value
,并且以字典的形式返回;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
2、批量赋值操作
同样,也可以通过KVC进行批量操作,使用对象调用setValuesForKeysWithDictionary:
方法时,可以传入一个包好key
、value
的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value
给对象的属性赋值。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
3、示例:
Person *person01 = [[Person alloc]init];
[person01 setValue:@"buer" forKey:@"name"];
[person01 setValue:@11 forKey:@"age"];
[person01 setValue:@0 forKey:@"sex"];
NSDictionary *dic01 = [person01 dictionaryWithValuesForKeys:@[@"name",@"age",@"sex"]];
NSLog(@"dic01 = %@",dic01);
NSDictionary *dic02 = @@"name":@12,@"age":@11,@"sex":@"1";
Person *person02 = [[Person alloc]init];
[person02 setValuesForKeysWithDictionary:dic02];
NSLog(@"name = %@,age = %@,sex = %ld",person02.name,person02.age,(long)person02.sex);
输出:
dic01 =
age = 11;
name = buer;
sex = 0;
name = 12,age = 11,sex = 1
四、使用KVC进行字典转模型
可以使用setValuesForKeysWithDictionary:
进行字典转模型。遇到没有定义的Key值属性时,可以重写方法setValue:forUndefinedKey:
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
五、异常信息和异常处理
当根据KVC
搜索规则,没有搜索到对应的key
或者keyPath
,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException
异常,并且应用程序Crash
。
我们可以重写下面两个方法,根据业务需求合理的处理KVC导致的异常:
- 在取值时,未有对应的key:
- (nullable id)valueForUndefinedKey:(NSString *)key;
- 在赋值时,未有对应的key:
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
当通过KVC
给某个非对象的属性赋值为nil时,例如使用setValue:forkey
给int
或float
类型属性赋值为nil
时,会抛出NSInvalidArgumentException
的异常并崩溃。
我们可以重写下面的方法,进行处理:
- (void)setNilValueForKey:(NSString *)key;
注意:
- Json字典里的字段数量和字段名字应该和Model类所匹配,Json少了字段不会出现问题,但是多了或者字段名不对,就会崩溃。需重写
setValue:forUndefinedKey:
方法;- 不需要对基本数据类型做处理,例如
int
型转NSNumber
,内部会自动作处理。但是定义NSString
类型的接收int型数据,会自动转成NSNumber
类型,给label复赋值的时候需要注意;NSArray
和NSDictionary
等集合对象,value
都不能为nil
,否则会导致Crash
;- 未定义的属性,使用
NSArray
和NSDictionary
集合接收,会转成NSNull类型;
六、使用KVC进行集合类的运算
KVC
提供的valueForKeyPath:
方法非常强大,可以在keyPath
中嵌套集合运算符对集合中的对象进行相关的运算,例如求一个数组中所有Person
对象的age
总和。集合对象主要指NSArray
和NSSet
,不包括NSDictionary
。
1、集合运算符的格式
keyPathToCollection.@collentionOperator.keyPathToproperty
- keyPathToCollection:Left key path,要操作的集合对象,若调用valueForKeyPath:方法的对象本来就是集合对象,则可以省略;
- collentionOperator:Collection operator,集合操作符,一般以@开头;
- keyPathToproperty:Right key path,要运算的属性。
//Address.h
@interface Address : NSObject
@property (copy, nonatomic) NSString *city;
@property (copy, nonatomic) NSString *street;
@property (strong, nonatomic) NSNumber *cityNumber;
@property (assign, nonatomic) NSInteger streetNumber;
@end
- (void)viewDidLoad
[super viewDidLoad];
NSMutableArray *array = [NSMutableArray array];
for ( int i = 0 ; i < 5 ; i++ )
Address *address = [[Address alloc] init];
NSString *cityStr = [NSString stringWithFormat:@"city-%d",i];
//批量赋值
NSDictionary *dic = @
@"city":cityStr,
@"street":@"street",
@"cityNumber":@(i),
@"streetNumber":@101
;
[address setValuesForKeysWithDictionary:dic];
[array addObject:address];
//返回数组中保存的对象的属性,返回值为数组
NSArray *cityArray = [array valueForKeyPath:@"city"];
NSLog(@"%@",cityArray);
2、集合运算符的分类
集合运算符主要分为三类:
- 集合操作符:处理集合包含的对象,并根据操作符的不同返回不同的类型,返回值以
NSNumber
为主。 - 数组操作符:根据操作符的条件,将符合条件的对象包含在数组中返回。
- 嵌套操作符:处理集合对象中嵌套其他集合对象的情况,返回结果也是一个集合对象。
(1)、集合操作符
集合操作符处理NSArray
和NSSet
及其子类这样的集合对象,并根据不同的操作符返回不同类型的对象,返回值一般都是NSNumber
。
①、@avg
用来计算集合中 right keyPath 指定的属性的平均值。
//@avg:平均值
NSNumber *avg = [array valueForKeyPath:@"@avg.streetNumber"];
NSLog(@"%@",avg);
②、@count
用来计算集合中对象的数量。备注:@count 操作符比较特殊,它不需要写 right keyPath,即使写了也会被忽略。
//@count:集合里对象的数量
NSNumber *count = [array valueForKeyPath:@"@count"];
NSLog(@"%@",count);
③、@sum
用来计算集合中 right keyPath 指定的属性的总和。
//@sum:总和
NSNumber *sum = [array valueForKeyPath:@"@sum.streetNumber"];
NSLog(@"%@",sum);
④、@max
用来查找集合中 right keyPath 指定的属性的最大值。
//@max:最大值
NSNumber *max = [array valueForKeyPath:@"@max.cityNumber"];
NSLog(@"%@",max);
⑤、@min
用来查找集合中 right keyPath 指定的属性的最小值。
//@min:最小值
NSNumber *min = [array valueForKeyPath:@"@min.cityNumber"];
NSLog(@"%@",min);
备注:@max 和 @min 在进行判断时,都是通过调用 compare: 方法进行判断,所以可以通过重写该方法对判断过程进行控制。
(2)、数组操作符(因为返回的是数组,所以叫数组操作符)
①、@unionOfObjects
将集合中的所有对象的同一个属性放在数组中返回。这个和直接写属性好像没区别。
NSArray *city = [array valueForKeyPath:@"@unionOfObjects.city"];
NSLog(@"%@",city);
NSArray *cityArray = [array valueForKeyPath:@"city"];
NSLog(@"%@",cityArray);
②、@distinctUnionOfObjects
将集合中对象的属性进行去重并返回。
NSArray *streetNumberArr = [array valueForKeyPath:@"@distinctUnionOfObjects.streetNumber"];
NSLog(@"%@",streetNumberArr);
(3)、嵌套操作符
嵌套操作符是对集合里的集合进行操作,比如数组里面的数组。
NSMutableArray *array1 = [NSMutableArray array];
for ( int i = 0 ; i < 5 ; i++ )
Address *address = [[Address alloc] init];
NSString *cityStr = [NSString stringWithFormat:@"city-%d",i];
//批量赋值
NSDictionary *dic = @
@"city":cityStr,
@"street":@"street",
@"cityNumber":@(i),
@"streetNumber":@101
;
[address setValuesForKeysWithDictionary:dic];
[array1 addObject:address];
NSMutableArray *array2 = [NSMutableArray array];
for ( int i = 3 ; i < 9 ; i++ )
Address *address = [[Address alloc] init];
NSString *cityStr = [NSString stringWithFormat:@"city-%d",i];
//批量赋值
NSDictionary *dic = @
@"city":cityStr,
@"street":@"street",
@"cityNumber":@(i),
@"streetNumber":@101
;
[address setValuesForKeysWithDictionary:dic];
[array2 addObject:address];
NSArray *array = @[array1,array2];
①、@unionOfArrays
是用来操作集合内部的集合,将所有right keyPath对应的属性放在一个数组中返回。
NSArray *result = [array valueForKeyPath:@"@unionOfArrays.city"];
②、@distinctUnionOfArrays
是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个数组中,并进行排重。
NSArray *result = [array valueForKeyPath:@"@distinctUnionOfArrays.city"];
③、@distinctUnionOfSets
是用来操作集合内部的集合对象,将所有right keyPath对应的对象放在一个set中,并进行排重。
NSSet *result = [array valueForKeyPath:@"@distinctUnionOfSets .city"];
七、KVC 的搜索规则
在学习KVC的搜索规则前,要先弄明白一个属性的作用,这个属性在搜索过程中祈祷很重要的作用。这个属性表示是否允许读取实例变量的值,如果为YES则在KVC查找的过程中,从内存中读取属性实例变量的值。
@property (class, readonly) BOOL accessInstanceVariablesDirectly;
在KVC的实现中,依赖setter和getter的方法实现,所以方法命名应该符合苹果要求的规范,否则会导致KVC失败。
1、基础Getter搜索模式(valueForKey:/valueForKeyPath:)
- 首先按get、、is的顺序查找getter方法,找到直接调用。
- 若方法的返回结果类型为是一个对象指针,则直接返回结果;
- 若类型为能够转化为NSNumber的基本数据类型,转换为NSNumber后返回;
- 否则,转换为NSValue返回。
- 上面的getter没有找到,查找countOf、objectInAtIndex:、AtIndexes格式的方法。
如果countOf<Key>
和另外两个方法中的一个找到,那么就会返回一个可以响应NSArray
所有方法的集合代理(collection proxy object)。发送给这个代理集合(collection proxy object)的NSArray消息方法,就会以countOf<Key>
、objectIn<Key>AtIndex:
、<Key>AtIndexes
这几个方法组合的形式调用。如果receiver的类实现了get<Key>:range:
方法,给方法也会用于性能优化。 - 还没查到,那么查找
countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的集合代理(collection proxy object)。发送给这个代理集合(collection proxy object)的NSSet消息方法,就会以countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
组合的形式调用。 - 还是没查到,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按
_<key>
,_is<Key>
,<key>
,is<Key>
(注意大小写)的顺序直接搜索实例变量。如果搜索到了,则返回receiver相应实例变量的值。返回结果的处理见步骤1。 - 再没查到,调用
valueForUndefinedKey:
方法,报出异常。
总结一下:
1、先找相应的 getter 方法(get, , is, 或者 ),找到了则返回(对象类型直接返回,其它类型进行转换);
2、没有找到,则寻找 NSArray 相应的方法;
3、没有找到,则寻找 NSSet 相应的方法;
4、如果还没有,且 accessInstanceVariablesDirectly 类属性返回的是 YES,则去搜索实例变量(、_is、、is)。如果发现了,则返回;
5、还没有,则转到 valueForUndefinedKey: 方法并抛出异常。
2、基础Setter搜索模式
这是setValue:forKey:
的默认实现,给定输入参数value
和key
。试图在接收调用对象的内部,设置属性名为key
的value
,通过下面的步骤:
- 查找
set<Key>:
或_set<Key>
命名的setter
,按照这个顺序,如果找到的话,调用这个方法并将值传进去(根据需要进行对象转换)。 - 如果没有发现一个简单的
setter
,但是accessInstanceVariablesDirectly
类属性返回YES
,则查找一个命名规则为_<key>
、_is<Key>
、<key>
、is<Key>
的实例变量。根据这个顺序,如果发现则将value赋值给实例变量。 - 如果没有发现setter或实例变量,则调用
setValue:forUndefinedKey:
方法,并默认提出一个异常,但是一个NSObject
的子类可以提出合适的行为
总结:
- 先找 setter 方法(set:或_set);
- 没找到,如果则 accessInstanceVariablesDirectly 类属性返回的是 YES,则去查找实例变量(_、_is、、is),若找到,则赋值;
- 没有找到 setter 方法和实例变量,则转到 setValue:forUndefinedKey: 方法,并抛出异常。
以上是关于iOS KVC的主要内容,如果未能解决你的问题,请参考以下文章