iOS KVC

Posted xiaoxiaobukuang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS KVC相关的知识,希望对你有一定的参考价值。

一、KVC简介

  • KVC全称是Key Value Coding(键值编码),是一个基于NSKeyValueCoding非正式协议实现的机制,他可以直接通过Key值对对象的属性存取操作,而不需通过明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定。
  • KVC提供了一种间接访问属性方法或成员变量的机制,可以通过字符串来访问对象的属性方法或成员变量;
  • 在实现了访问器方法的类中,使用点语法和KVC访问对象其实差别不大,二者可以任意混用(因为KVC会首先搜索访问器方法,见下文)。但是没有访问器的类种,点语法无法使用,这时KVC就有优势了。
  • KVCKVO都是基于OC的动态特性和Runtime机制的。

二、KVC通用的访问方法

1、通用的访问方法

  • getter方法
- (nullable id)valueForKey:(NSString *)key;
  • setter方法
- (void)setValue:(nullable id)value forKey:(NSString *)key;

2、衍生的KeyPath方法,用来进行深层访问(Key使用点语法),也可以单层访问

  • keyPathsetter方法:
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
  • keyPathgetter方法:
- (nullable id)valueForKeyPath:(NSString *)keyPath;

iOS-KVC实现

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:方法时,可以传入一个包好keyvalue的字典进去,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:forkeyintfloat类型属性赋值为nil时,会抛出NSInvalidArgumentException的异常并崩溃。
我们可以重写下面的方法,进行处理:

- (void)setNilValueForKey:(NSString *)key;

注意:

  • Json字典里的字段数量和字段名字应该和Model类所匹配,Json少了字段不会出现问题,但是多了或者字段名不对,就会崩溃。需重写setValue:forUndefinedKey:方法;
  • 不需要对基本数据类型做处理,例如int型转NSNumber,内部会自动作处理。但是定义NSString类型的接收int型数据,会自动转成NSNumber类型,给label复赋值的时候需要注意;
  • NSArrayNSDictionary等集合对象,value都不能为nil,否则会导致Crash
  • 未定义的属性,使用NSArrayNSDictionary集合接收,会转成NSNull类型;

六、使用KVC进行集合类的运算

KVC提供的valueForKeyPath:方法非常强大,可以在keyPath中嵌套集合运算符对集合中的对象进行相关的运算,例如求一个数组中所有Person对象的age总和。集合对象主要指NSArrayNSSet,不包括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)、集合操作符

集合操作符处理NSArrayNSSet及其子类这样的集合对象,并根据不同的操作符返回不同类型的对象,返回值一般都是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:)

  1. 首先按get、、is的顺序查找getter方法,找到直接调用。
  • 若方法的返回结果类型为是一个对象指针,则直接返回结果;
  • 若类型为能够转化为NSNumber的基本数据类型,转换为NSNumber后返回;
  • 否则,转换为NSValue返回。
  1. 上面的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:方法,给方法也会用于性能优化。
  2. 还没查到,那么查找countOf<Key>enumeratorOf<Key>memberOf<Key>:格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所有方法的集合代理(collection proxy object)。发送给这个代理集合(collection proxy object)的NSSet消息方法,就会以countOf<Key>enumeratorOf<Key>memberOf<Key>:组合的形式调用。
  3. 还是没查到,那么如果类方法accessInstanceVariablesDirectly返回YES,那么按_<key>_is<Key><key>is<Key>(注意大小写)的顺序直接搜索实例变量。如果搜索到了,则返回receiver相应实例变量的值。返回结果的处理见步骤1。
  4. 再没查到,调用valueForUndefinedKey:方法,报出异常。

总结一下:
1、先找相应的 getter 方法(get, , is, 或者 ),找到了则返回(对象类型直接返回,其它类型进行转换);
2、没有找到,则寻找 NSArray 相应的方法;
3、没有找到,则寻找 NSSet 相应的方法;
4、如果还没有,且 accessInstanceVariablesDirectly 类属性返回的是 YES,则去搜索实例变量(
、_is、、is)。如果发现了,则返回;
5、还没有,则转到 valueForUndefinedKey: 方法并抛出异常。

2、基础Setter搜索模式

这是setValue:forKey:的默认实现,给定输入参数valuekey。试图在接收调用对象的内部,设置属性名为keyvalue,通过下面的步骤:

  • 查找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的主要内容,如果未能解决你的问题,请参考以下文章

iOS KVC详细讲解

ios26--kvc

iOS核心笔记—KVC机制

iOS开发-OC篇-KVC详解

原iOS中KVC和KVO的区别

iOS底层探索之KVC