KVC 底层原理探索

定义

KVC的全称是Key-Value Coding,翻译成中文是 键值编码,键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来间接访问其属性。既可以通过一个字符串key来访问某个属性。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。

KVC 相关API

常用方法

主要有以下四个常用的方法

  • 通过key 设值/取值
//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;

//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;

  • 通过keyPath (即路由)设值/取值
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; 

//通过KeyPath来设值                 
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

其他方法

//默认返回YES,表示如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;

//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

KVC 设值 底层原理

在日常开发中,针对对象属性的赋值,一般有以下两种方式

  • 直接通过setter方法赋值
  • 通过KVC键值编码赋值
ZGPerson *person = [[ZGPerson alloc] init];
// 1、一般setter 方法
person.name      = @"张三";
// 2、KVC方式
[person setValue:@"李四" forKey:@"name"]; 

下面针对使用最多的KVC设值方法:setValue:forKey,来进行其底层原理的探索。

首先进入setValue:forKey的声明,发现是在Foundation框架中,而Foundation框架是不开源的,所以我们通过Key-Value Coding Programming Guide苹果官方文档来研究

KVC 设值setValue

当调用setValue:forKey:设置属性value时,其底层的执行流程为

  • 【第一步】首先查找是否有这三种setter方法,按照查找顺序为set:-> _set -> setIs

    • 如果有其中任意一个setter方法,则直接设置属性的value(主注意:key是指成员变量名,首字符大小写需要符合KVC的命名规范)

    • 如果都没有,则进入【第二步】

  • 【第二步】:如果没有第一步中的三个简单的setter方法,则查找accessInstanceVariablesDirectly是否返回YES

    • 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:_ -> _is -> -> is

      • 如果找到其中任意一个实例变量,则赋值

      • 如果都没有,则进入【第三步】

    • 如果返回NO,则进入【第三步】

  • 【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

综上所述,KVC通过 setValue:forKey: 方法设值的流程以设置LGPerson的对象person的属性name为例,如下图所示

KVC 设值
KVC 取值 底层原理

同样的,我们可以通过官方文档分析KVC取值的底层原理

当调用valueForKey:时,其底层的执行流程如下

  • 【第一步】首先查找getter方法,按照get -> -> is -> _的方法顺序查找,

    • 如果找到,则进入【第五步】

    • 如果没有找到,则进入【第二步】

  • 【第二步】如果第一步中的getter方法没有找到,KVC会查找countOf 和objectIn AtIndex :和 AtIndexes :

    • 如果找到countOf 和其他两个中的一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即NSKeyValueArray,是NSArray子类。代理对象随后将接收到的所有NSArray消息转换为countOf,objectIn AtIndex:和AtIndexes:消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get:range:之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)

    • 如果没有找到这三个访问数组的,请继续进入【第三步】

  • 【第三步】如果没有找到上面的几种方法,则会同时查找countOf ,enumeratorOf和memberOf这三个方法

    • 如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOf,enumeratorOf和memberOf消息的某种组合,用于创建它的对象

    • 如果还是没有找到,则进入【第四步】

  • 【第四步】如果还没有找到,检查类方法InstanceVariablesDirectly是否YES,依次搜索_,_is或is的实例变量

    • 如果搜到,直接获取实例变量的值,进入【第五步】
  • 【第五步】根据搜索到的属性值的类型,返回不同的结果

    • 如果是对象指针,则直接返回结果

    • 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它

    • 如果是是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象

  • 【第六步】如果上面5步的方法均失败,系统会执行该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

综上所述,KVC通过 valueForKey: 方法取值的流程以设置LGPerson的对象person的属性name为例,如下图所示

KVC 取值

使用keyPath

在实际开发过程中,一个类的成员变量有可能是自定义类或者其他的复杂数据类型,我们可以先用KVC获取该属性,然后再用KVC来获取这个自定义类的属性。但这样比较繁琐,因此KVC提供了一个解决方案,keyPath

// 通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
// 通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

下面举个例子

@interface ZGPerson : NSObject
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger *sex;
@property (strong, nonatomic) NSNumber *age;
@property (strong, nonatomic) Address *address;
@end
......分割线
@interface Address : NSObject
@property (copy, nonatomic) NSString *city;
@property (copy, nonatomic) NSString *street;
@end
......分割线
- (void)viewDidLoad {
    [super viewDidLoad];
    ZGPerson *myself = [[ZGPerson alloc] init];

    [myself setValue:@"ysy" forKey:@"name"];

    NSLog(@"-------name = %@",myself.name);
    NSLog(@"-------name = %@",[myself valueForKey:@"name"]);

    /**
     keyPath的setter方法:setValue: forKeyPath:
     keyPath的getter方法:valueForKeyPath:

     keyPath为多级访问,使用点语法
     */

    //注意,这里要想使用keypath对adress的属性进行赋值,必须先给myself赋一个Address对象
    Address *myAddress = [[Address alloc] init];

    [myself setValue:myAddress forKey:@"address"];

    //KeyPath为多级访问
    [myself setValue:@"shanghai" forKeyPath:@"address.city"];

    NSLog(@"-------city = %@",myself.address.city);

    NSLog(@"-------city = %@",[myself valueForKeyPath:@"address.city"]);
    
}

异常处理

使用KVC过程中最常见的异常就是不小心使用了错误的key,或者在设值时不小心传了nil的值,KVC有特定的方法处理这些异常。

  • KVC处理nil异常,如果在设值过程中,不小心传了nil值,KVC会调用方法 setNilValueForKey:,这个默认方法是抛出 NSInvalidArgumentException 异常,所以一般而言最好重写这个方法,对异常进行处理。
  • KVC处理UndefinedKey异常,如果在设值取值传的key不存在时,程序就会crash,设值会调用到 setValue:forUndefinedKey:方法,而取值会调用valueForUndefinedKey:方法,这两个方法默认都是抛出 NSUndefinedKeyException异常,因此如果要避免程序crash,可以重写这两个方法。

KVC 的多值操作

批量取值操作

KVC还有更强大的功能,可以根据给定的一组key,获取到一组value,并且以字典的形式返回。

- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

批量赋值操作

同样,也可以通过KVC进行批量赋值。使用对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含key、value的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。

- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

示例

示例如下:

//批量赋值
NSDictionary *dic = @{
                      @"name":@"xiaoMing",
                      @"sex":@1,
                      @"age":@12,
                      @"address":myAddress
                      };

[myself setValuesForKeysWithDictionary:dic];

//批量取值
NSArray *keys = @[@"name",@"age",@"sex",@"address"];

NSDictionary *values = [myself dictionaryWithValuesForKeys:keys];

NSLog(@"%@",values);

输出:

{
    address = "";
    age = 12;
    name = xiaoMing;
    sex = 1;
}

集合属性操作

当我们要操作一个对象里的集合属性时(NSArray、NSSet等),我们可以通过KVC方法取到集合属性,然后通过集合属性操作集合中的元素。实际开发中最常用的方法,叫做间接操作。

直接操作要实现下面的方法,但通常不会用到:

有序集合对应方法如下:

-countOf//必须实现,对应于NSArray的基本方法count:2  -objectInAtIndex:

-AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:

-insertObject:inAtIndex:

-insert:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFromAtIndex:

-removeAtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectInAtIndex:withObject:

-replaceAtIndexes:with://可选的,如果在此类操作上有性能问题,就需要考虑实现之

无序集合对应方法如下:

-countOf//必须实现,对应于NSArray的基本方法count:

-objectInAtIndex:

-AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:

-insertObject:inAtIndex:

-insert:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFromAtIndex:

-removeAtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectInAtIndex:withObject:

-replaceAtIndexes:with://这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之

使用 KVC 进行集合类的运算

KVC 提供的valueForKeyPath:方法非常强大,可以在keyPath中嵌套集合运算符对集合中的对象进行相关的运算,例如求一个数组中所有Person对象的age总和。集合对象主要指NSArray和NSSet,不包括NSDictionary

集合运算符的格式
[email protected]
  • keyPathToCollection:Left key path,要操作的集合对象,若调用 valueForKeyPath: 方法的对象本来就是集合对象,则可以省略;
  • collentionOperator:Collection operator,集合操作符,一般以@开头;
  • keyPathToproperty:Right key path,要运算的属性。
    举例
   Address *myAddress = [[Address alloc] init];
//
    [myself setValue:myAddress forKey:@"address"];

    //KeyPath为多级访问
    [myself setValue:@"shanghai" forKeyPath:@"address.city"];

    NSLog(@"-------city = %@",myself.address.city);

    NSLog(@"-------city = %@",[myself valueForKeyPath:@"address.city"]);
    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":@(100+i)
                              };

        [address setValuesForKeysWithDictionary:dic];

        [array addObject:address];
    }

    //返回数组中保存的对象的属性,返回值为数组
    NSArray *cityArray = [array valueForKeyPath:@"cityNumber"];
    NSLog(@"%@",cityArray);
    
    NSArray *streetNumberArray = [array valueForKeyPath:@"streetNumber"];
    //求平均值
    NSNumber *avg = [array valueForKeyPath:@"@avg.streetNumber"];
    NSLog(@"%@-%@",streetNumberArray,avg);

打印输出:

-------city = shanghai
2021-01-18 16:28:30.373550+0800 005---Runtime应用[73332:21674820] -------city = shanghai
2021-01-18 16:28:30.373673+0800 005---Runtime应用[73332:21674820] (
    0,
    1,
    2,
    3,
    4
)
2021-01-18 16:28:30.373814+0800 005---Runtime应用[73332:21674820] (
    100,
    101,
    102,
    103,
    104
)-102
集合运算符的分类

集合运算符主要分为三类:

  • 集合操作符:处理集合包含的对象,并根据操作符的不同返回不同的类型,返回值以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:方法进行判断,所以可以通过重写该方法对判断过程进行控制。
加上一些打印

    NSArray *streetNumberArray = [array valueForKeyPath:@"streetNumber"];
    NSNumber *avg = [array valueForKeyPath:@"@avg.streetNumber"];
    NSNumber *count = [array valueForKeyPath:@"@count"];
    NSNumber *sum = [array valueForKeyPath:@"@sum.streetNumber"];
    NSNumber *max = [array valueForKeyPath:@"@max.cityNumber"];
    NSNumber *min = [array valueForKeyPath:@"@min.cityNumber"];

输出:
2021-01-18 16:39:08.167698+0800 005---Runtime应用[73377:21677966] (
    100,
    101,
    102,
    103,
    104
)-avg:102
 count:5
 sum:510
 max:4
 min:0
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);
    NSArray *city = [array valueForKeyPath:@"@unionOfObjects.city"];
    NSLog(@"%@",city);
    
    NSArray *street= [array valueForKeyPath:@"@distinctUnionOfObjects.street"];
       NSLog(@"%@",street);

打印结果:

2021-01-18 16:44:22.055668+0800 005---KVC[73410:21680170] (
    street
)
2021-01-18 16:44:22.055726+0800 005---KVC[73410:21680170] (
    "city-0",
    "city-1",
    "city-2",
    "city-3",
    "city-4"
)
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":@(100+i)
                              };

        [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":@(100+i)
                              };

        [address setValuesForKeysWithDictionary:dic];

        [array2 addObject:address];
    }

    NSArray *array = @[array1,array2];
    
    NSArray *cityArray = [array valueForKeyPath:@"city"];
    NSArray *result = [array valueForKeyPath:@"@unionOfArrays.city"];
    NSArray *disResult = [array valueForKeyPath:@"@distinctUnionOfArrays.city"];
    NSSet *resultSet = [array valueForKeyPath:@"@distinctUnionOfSets.city"];
    NSLog(@"%@",cityArray);
    NSLog(@"%@",result);
    NSLog(@"%@",disResult);
    NSLog(@"%@",resultSet);
  • @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的实现原理,我们可以看出KVC的性能并不如直接访问属性快,虽然这个性能消耗是微乎其微的。所以在使用KVC的时候,建议最好不要手动设置属性的setter、getter,这样会导致搜索步骤变长。

而且尽量不要用KVC进行集合操作,例如NSArray、NSSet之类的,集合操作的性能消耗更大,而且还会创建不必要的对象。

你可能感兴趣的:(KVC 底层原理探索)