iOS 底层探索之KVC

1,135 阅读9分钟

参考官方文档

KVC概述

键值编码是一种由NSKeyValueCoding非正式协议启用的机制,对象采用该机制提供对其属性的间接访问。键值编码是一个基本概念,是许多其他Cocoa技术的基础,在某些情况下,键值编码还有助于简化代码。

KVC可以通过键值的方式对对象的属性进行存取操作,这使得在运行时操作对象的属性成为可能。

我们通常都是使用gettersetter方法去访问和设置属性的值,有了KVC我们可以直接访问属性的基础实例变量,当然这样可读性和性能并不是很高,一般不推荐这么去用。

KVC的使用大全

Part1、普通用法

1、取值valueForKey:

普通对象

[stu valueForKey:@"age"];

数组等集合对象的处理

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

2、赋值setValue:forKey:

[stu setValue:@(18) forKey:@"age"];

3、批量处理

给定字典可以根据keys的数组返回所有符合的键值对,以字典的形式返回 dictionaryWithValuesForKeyssetValuesForKeysWithDictionary.

@interface Person : 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;
@property (nonatomic, strong) Student *stu;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [Person new];
    NSDictionary* dict = @{
                           @"name":@"Tom",
                           @"age":@(18+i),
                           @"nick":@"Cat",
                           @"length":@(180 + 2*arc4random_uniform(6)),
                          };
    [p setValuesForKeysWithDictionary:dict];
}

@end

Part2、keyPath用法 1、将数组中所有对象的name属性值取出,并放入一个数组中返回

NSArray *names = [array valueForKeyPath:@"name"];

同理,也可以获取每个数组元素的length属性

- (void)arrayMessagePass{
    NSArray *array = @[@"First",@"Second",@"Third",@"Forth"];
    NSArray *lenStr= [array valueForKeyPath:@"length"];
    NSLog(@"lenStr = %@",lenStr);
    NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
    NSLog(@"lowStr = %@",lowStr);
}

打印如下

image.png

2、在keyPath里,key也可以使用点语法。

@interface Student : NSObject
{
    @public
    NSArray *books;
    NSString *name;
    int age;
}

@property (nonatomic, copy) NSString *sex;

@end

@interface Person : 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;
@property (nonatomic, strong) Student *stu;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p = [Person new];
    NSLog(@"%@", [p valueForKeyPath:@"stu.sex"]);
}

@end
容错处理

当KVC经过搜索模式没有获取到对应的值时,会调用相对应的异常方法,导致应用程序Crash,常见的异常有三种,为了防止出现这种严重的错误,我们通常的做法是重写相对应的异常方法,如下所示 1、'NSUnknownKeyException', reason: '[<Person 0x6000016f1e00> valueForUndefinedKey:]: this class is not key value coding-compliant for the key hahaName.'

- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"%@的值不能为空",key);
}

2、'NSUnknownKeyException', reason: '[<Person 0x6000008ffe80> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xixiName.'

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"不能对不存在的健赋值");
}

3、NSInvalidArgumentException', reason: '[<Person 0x60000295ca00> setNilValueForKey]:

- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"不能对不存在的键取值");
    return @"error";
}
KVC集合类运算

使用 KVC 进行集合类的运算,例如求一个数组中所有Person对象的length总和。

image.png
如图所示

  • keyPathToCollection:Left key path,要操作的集合对象,若调用 valueForKeyPath: 方法的对象本来就是集合对象,则可以省略;
  • collentionOperator:Collection operator,集合操作符,一般以@开头;
  • keyPathToproperty:Right key path,要运算的属性。

集合运算符分为三种:集合操作符(返回NSNumber)、数组操作符(返回数组)、嵌套操作符 Part1、集合操作符(Aggregation Operators) 返回的是NSNumber类型的值,有以下5种用法,分别是平均、总和、数量、最大、最小。

  • 1、@avg用来计算指定属性的平均值,返回NSNumber
  • 2、@sum属性总和,返回NSNumber
  • 3、@count用来计算集合里对象的数量
  • 4、@max属性最大值
  • 5、@min属性最小值

用法如下所示

NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        Person *p = [Person new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(180 + 2*arc4random_uniform(6)),
                               };
        [p setValuesForKeysWithDictionary:dict];
        [personArray addObject:p];
    }
    NSLog(@"%@", [personArray valueForKey:@"length"]);
    
    float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
    NSLog(@"集合中有length属性的元素,length属性的平均值为 = %f", avg);
    int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
    NSLog(@"集合中有length属性的元素的数量为 = %d", count);
    int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
    NSLog(@"集合中有length属性的元素,length属性的总和为 = %d", sum);
    int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
    NSLog(@"集合中有length属性的元素,length属性的最大值为 = %d", max);
    int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
    NSLog(@"集合中有length属性的元素,length属性的最小值为 = %d", min);

打印结果如下

image.png

max和min是通过compare:方法进行比对的,所以需要属性满足能通过compare方法比对

Part2、数组操作符(Array Operators) 与集合操作符的区别是,数组操作符返回值的是数组。它有以下2种用法

  • 1、@unionOfObjects返回指定属性的数组
  • 2、@distinctUnionOfObjects返回指定属性去重后的集合,即数组中payee属性值重复的都去掉(去重)。
NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        Person *p = [Person 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);

打印结果如下:

image.png
从结果中我们知道,@unionOfObjects的作用是返回数组元素中符合length属性的元素;而@distinctUnionOfObjects在此基础上做了一步去重操作。

Part3、嵌套数组

嵌套运算符在嵌套集合上运行,集合的每个元素本身都包含一个集合。它有以下2种用法

  • 1、@unionOfArrays:valueForKeyPath创建并返回一个数组,该数组包含和属性对应的集合的所有对象,且不删除重复项
  • 2、@distinctUnionOfArrays:valueForKeyPath创建并返回一个数组,该数组包含和属性对应的所有集合的组合的不同对象,就是去重。
NSMutableArray *personArray1 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personArray1 addObject:person];
    }
    
    NSMutableArray *personArray2 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGPerson *person = [LGPerson new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [person setValuesForKeysWithDictionary:dict];
        [personArray2 addObject:person];
    }
    
    // 嵌套数组
    NSArray* nestArr = @[personArray1, personArray2];
    
    NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
    NSLog(@"unionOfArrays = %@", arr1);
    
    NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
    NSLog(@"distinctUnionOfArrays = %@", arr);

打印结果如下:

image.png

从结果中我们可以得知,@unionOfArrays的作用是将所有嵌套的集合,通过属性全部整合到一个集合中了;而@distinctUnionOfArrays在此基础上又做了一步去重操作。

KVC的搜索模式

Part1、基于Getter的搜索模式

  • 1、先按顺序查找访问方法getKey,key,isKey,或者_key。
  • 2、如果getter方法没有找到,则尝试寻找countOfKey、objectInKeyAtIndex:、KeyAtIndexes,如果找到了第一个和剩下两个中的一个方法,就会返回一个数组
  • 3、如果还是没有找到,则尝试countOfKey,enumeratorOfKey和memberOfKey:,如果上述三个方法都找到了,就去创建一个NSSet类型的对象,则会返回一个可以响应NSSet所有方法的对象。
  • 4、如果上述方式没有找到,判断accessInstanceVariablesDirectly的值为YES时,在内存中搜索_key,_isKey,key,或者isKey,找到了就执行5,没找到就执行步骤6.
  • 5、返回结果
  • 6、如果都失败了,调用valueForUndefinedKey:抛出异常。

举个例子看比较好理解一些

@interface Student : NSObject
{
    @public
    NSArray *books;
    NSString *name;
    int age;
}

@property (nonatomic, copy) NSString *sex;

@end

@implementation Student

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Student *stu = [Student new];
    stu->name = @"stupid bird";
    NSLog(@"name = %@", [stu valueForKey:@"name"]);
    //[stu setValue:@(18) forKey:@"age"];
}

@end

从代码中我们知道Student类包含了一个名为name的实例变量,它默认是没有实现settergetter方法的,在ViewDidLoad中给stuname属性赋值,再通过valueForKey:的方式取值,看看打印的结果是什么?

image.png
可以看到,获取到了name属性的结果,我们对比上面的Getter的搜索顺序,不难发现它走到了第4步,从内存中获取的name属性值。

这一次我们在Student类中增加两个方法

@implementation Student
//name****************
//返回name属性的个数
- (NSUInteger)countOfName {
    NSLog(@"%s", __func__);
    return 5;
}

//返回每个name属性对应的值
- (NSString *)objectInNameAtIndex:(NSUInteger)index {
    return [NSString stringWithFormat:@"name %lu", index];
}

@end

打印结果如下:

image.png

我们发现获取到的并不是name属性的结果,对比上面的Getter的搜索顺序,发现它是执行到了第2步,直接返回了一个属性为name的数组。

以上就是探索基于Getter的搜索模式

Part2、基于Setter的搜索模式

  • 1、按顺序查找名为set<Key>:_set<Key>方法,如果找到,则使用该方法进行赋值。
  • 2、如果没有找到,且accessInstanceVariablesDirectly返回YES,则在内存中查找_<key>_is<Key><key>,或者is<Key>的实例变量,按照这个顺序。如果找到,则直接赋值。
  • 3、如果还找不到,则调用setValue:forUndefinedKey:抛出异常。

以上是官方文档说的Setter赋值时的搜索模式,我们用代码实验一下

@interface Person : NSObject
@end

@implementation Person

#pragma mark - set相关
- (void)setName:(NSString *)name{
    NSLog(@"%s",__func__);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s",__func__);
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGPerson *p = [[LGPerson alloc] init];
    [p setValue:@"name" forKey:@"name"];
//    NSLog(@"%@",[p valueForKey:@"name"]);
}


@end

按照官方说的搜索步骤,先按顺序查找名为set<Key>:_set<Key>方法,看看打印结果

image.png
注释掉setName:方法试试

@implementation Person

//#pragma mark - set相关
//- (void)setName:(NSString *)name{
//    NSLog(@"%s",__func__);
//}

- (void)_setName:(NSString *)name{
    NSLog(@"%s",__func__);
}

@end

打印结果如下

image.png
好的,打印结果上看是从_setName方法中获取的,官方说的没错(噗)。

为了验证下面步骤,按如下修改Person类

@interface LGPerson : NSObject{
    @public
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
}
@end

@implementation Person
@end

viewDidLoad中打断点,如下所示

image.png
发现_name属性被赋值了,所以官方说的对。 至此,就不再去质疑官方了,我记住了,在KVC进行Setter方法赋值时,若没有setter方法,会按照_<key>、_is<key>、<key>、is<key>的顺序赋值。(偷笑)

写这篇的目的是更好的理解KVC的机制,希望和大家一起进步。