iOS底层探索-KVC原理

679 阅读5分钟

KVC 简介 官方文档

  • Key-Value Coding 键值编码是一种由NSKeyValueCoding非正式协议启用的机制,对象采用它来提供对其属性的间接访问。当一个对象符合键值编码时,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数来寻址。这种间接访问机制补充了实例变量及其关联的访问方法所提供的直接访问。

KVC 的使用

开发中会经常使用KVC,例如对象属性赋值,StoryBoard添加User Defined Runtime Attributed属性等

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;

1. KVC - 基本类型

LGPerson *person = [[LGPerson alloc] init];
person.name      = @"17"
person.age       = 18;
person->myName   = @"19";
NSLog(@"%@ - %d - %@",person.name,person.age,person->myName);
[person setValue:@"16" forKey:@"name"];
NSLog(@"%@ - %d - %@",person.name,person.age,person->myName);

结果:

image.png

2. KVC - 集合类型

  • 方法一
person.array = @[@"1",@"2",@"3"];
NSLog(@"%@",[person valueForKey:@"array"]);
NSArray *array = [person valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);

结果:

image.png

  • 方法二
person.array = @[@"1",@"2",@"3"];
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);

结果:

image.png

3. KVC - 集合操作符

  • 聚合操作符
#pragma mark - 聚合操作符
// @avg、@count、@max、@min、@sum
- (void)aggregationOperator{
    NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *p = [LGStudent 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);
}
  • 数组操作符
// 数组操作符 @distinctUnionOfObjects @unionOfObjects
- (void)arrayOperator{
    NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *p = [LGStudent 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)
}
  • 嵌套集合操作符
// 嵌套集合(array&set)操作 @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets
- (void)arrayNesting{
    NSMutableArray *personArray1 = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        LGStudent *student = [LGStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom",
                               @"age":@(18+i),
                               @"nick":@"Cat",
                               @"length":@(175 + 2*arc4random_uniform(6)),
                               };
        [student setValuesForKeysWithDictionary:dict];
        [personArray1 addObject:student];
    }
    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* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
    NSLog(@"arr = %@", arr);
    NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
    NSLog(@"arr1 = %@", arr1);
}

4. KVC - 访问非对象属性

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);

结果:

image.png

5.:KVC - 层层访问 - keyPath

LGStudent *student = [LGStudent alloc];
student.subject    = @"1234";
person.student     = student;
[person setValue:@"Swift" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

结果:

image.png

KVC 设值和取值过程 官方文档

设值原理

基本Setter的搜索模

setValue:forKey:的默认实现,给定keyvalue参数作为输入,尝试在接收调用的对象内部设置名为key的属性为value(或者,对于非对象属性,value的未包装版本,如表示非对象值所述),使用如下过程:

  1. 查找第一个名为set<Key>:_set<Key>的访问器,按此顺序。如果找到,则使用输入值(或根据需要取消包装的值)调用它并完成。

  2. 如果没有找到简单的访问器,并且如果类方法accessinstancevariablesdirect返回YES,请查找一个实例变量,其名称依次为_<key>_is< key><key>,或is< key>。如果找到,直接用输入值(或取消包装的值)设置变量并完成。

  3. 在没有找到访问器或实例变量时,调用setValue:forUndefinedKey:。默认情况下,这会引发一个异常,但是NSObject的子类可能提供特定于键的行为。

设值流程图

未命名文件 (5).png

取值原理

基本Getter的搜索模式

valueForKey:的默认实现,给定一个key参数作为输入,执行以下过程,从接收valueForKey: call的类实例内部操作。

  1. 在实例中搜索第一个名称为get<Key><Key>is<Key>_< Key>的访问器方法。如果找到了,就调用它,并使用结果继续步骤5。否则执行下一步。

  2. 如果没有找到简单的访问器方法,在实例中搜索匹配模式countOf<Key>objectIn<Key>AtIndex:(对应于NSArray类定义的原始方法)和<Key> AtIndexes:(对应于NSArray方法objectsAtIndexes:)的方法。 如果找到了第一个和另外两个中的至少一个,创建一个集合代理对象,响应所有NSArray方法并返回它。否则,执行步骤3。 代理对象随后将它接收到的所有NSArray消息转换为countOf<Key>objectIn<Key>AtIndex:,和<Key> AtIndexes:消息的组合,并将其转换为符合键值编码的创建它的对象。如果原始对象还实现了一个名称为get<Key>:range:的可选方法,那么代理对象也会在适当的时候使用该方法。实际上,代理对象与符合键值编码的对象一起工作,允许底层属性像NSArray一样行为,即使它不是NSArray。

  3. 如果没有找到简单的访问方法或数组访问方法组,则查找名为countOf<Key>enumeratorOf<Key>,和memberOf<Key>:(对应于NSSet类定义的基本方法)的三个方法。 如果找到了所有三个方法,创建一个集合代理对象响应所有的NSSet方法并返回它。否则,执行步骤4。 这个代理对象随后将它接收到的任何NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>: messages的组合,并将其转换为创建它的对象。实际上,代理对象与符合键值编码的对象一起工作,允许底层属性的行为就像它是一个NSSet,即使它不是。

  4. 如果没有找到简单的访问方法或一组集合访问方法,并且如果接收方的类方法accessinstancevariablesdirect返回YES,则搜索实例变量_<key>_is< key><key>,或is< key>,依次。如果找到,直接获取实例变量的值,然后继续步骤5。否则,执行步骤6。

  5. 如果检索到的属性值是对象指针,只需返回结果。 如果该值是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回该实例。 如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回该对象。

  6. 如果其他方法都失败了,调用valueForUndefinedKey:。默认情况下,这会引发一个异常,但是NSObject的子类可能提供特定于键的行为。

取值流程图

未命名文件 (6).png

KVC 异常处理

根绝KVC搜索规则,当没有的对应的key或者keyPath时(例如:字典取值,获取对象赋值),会抛出异常NSUnknownKeyException并且程序Crash

设值Crash

LGPerson *p = [[LGPerson alloc] init];
[p setValue:@"1" forKey:@"name111"];

结果: image.png

设值空值

LGPerson *p = [[LGPerson alloc] init];
NSString *string = nil;
[p setValue:string forKey:@"name"];
NSLog(@"%@",[p valueForKey:@"name"]);

结果: image.png

取值Crash

LGPerson *p = [[LGPerson alloc] init];
[p valueForKey:@"name111"];

结果: image.png

异常处理

苹果提供相应的异常处理方法

/* Given that an invocation of -valueForKey: would be unable to get a keyed value using its default access mechanism, return the keyed value using some other mechanism. The default implementation of this method raises an NSUndefinedKeyException. You can override it to handle properties that are dynamically defined at run-time.*/

- (**nullable** **id**)valueForUndefinedKey:(NSString *)key;

/* Given that an invocation of -setValue:forKey: would be unable to set the keyed value using its default mechanism, set the keyed value using some other mechanism. The default implementation of this method raises an NSUndefinedKeyException. You can override it to handle properties that are dynamically defined at run-time.*/

- (**void**)setValue:(**nullable** **id**)value forUndefinedKey:(NSString *)key;

/* Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism. The default implementation of this method raises an NSInvalidArgumentException. You can override it to map nil values to something meaningful in the context of your application.*/

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

在目标类中重新相应的方法即可避免Crash,但是这样就会很繁琐,可以添加到统一父类中或者添加的分类中

image.png