iOS底层探索 -- KVC 底层原理分析

前言

  在日常的开发中,在对数据进行处理中,常常使用三方框架将其转换为模型 (model),以方便使用点语法进行调用。这些框架底层都是运用的KVC(Key-Value Coding),今天来探索一下KVC底层的原理。

1.   KVC(Key-Value Coding)初探

   KVCKey-Value Coding,翻译过来就是键值编码,关于这个概念,具体可以查看Apple的官方文档

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties.

【译】键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。

当对象符合键值编码时,可以通过简洁,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。

当对象继承自NSObject时,可以通过简洁的方法(setValue:forKeyvalueForKey)实现以下操作:

  • 访问对象属性,例如通过getter valueForKey:setter setValue:forKey:,用于通过名称或键(参数化为字符串)来访问对象属性。
  • 操作集合属性,比如:NSArray
  • 在集合对象上调用集合运算符
  • 访问非对象属性,比如:结构体
  • 通过键路径访问属性

2.   KVC 深入

通常在我们给对象声明属性时,会有一下几类:

  • 属性。这些是简单的值,例如标量,字符串或布尔值。值对象(例如)NSNumber和其他不可变类型(例如)NSColor也被视为属性。

  • 一对一的关系。这些是具有自己属性的可变对象对象的属性可以更改,而无需更改对象本身。

      例如,银行帐户对象可能具有所有者属性,该属性是Person对象的实例,该对象本身具有地址属性。所
      有者的地址可以更改,而无需更改银行帐户持有的所有者
    
  • 一对多关系。这些是集合对象。比如:NSArrayNSSet

2.1  访问对象属性

  1. 通过 valueForKey:setValue:ForKey: 来间接的获取和设置属性值
[person setValue:@"KC" forKey:@"name"];
[person setValue:@19 forKey:@"age"];
[person setValue:@"酷C" forKey:@"myName"];

NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);
  • setValue:forKey: - Sets the value of the specified key relative to the object receiving the message to the given value. The default implementation of setValue:forKey: automatically unwraps NSNumber and NSValue objects that represent scalars and structs and assigns them to the property. See Representing Non-Object Values for details on the wrapping and unwrapping semantics.
    If the specified key corresponds to a property that the object receiving the setter call does not have, the object sends itself a setValue:forUndefinedKey: message. The default implementation of setValue:forUndefinedKey: raises an NSUndefinedKeyException. However, subclasses may override this method to handle the request in a custom manner.

【译】将相对于接收消息的对象的指定键的值设置为给定值。setValue:forKey:自动实现代表标量和结构的自动包装NSNumberNSValue对象的默认实现,并将其分配给属性。有关包装和展开语义的详细信息,请参见表示非对象值。
如果指定的键对应于接收setter调用的对象所不具有的属性,则该对象向自身发送setValue:forUndefinedKey:消息。的默认实现setValue:forUndefinedKey:会引发NSUndefinedKeyException。但是,子类可以重写此方法以自定义方式处理请求。

  • valueForKey: - Returns the value of a property named by the key parameter. If the property named by the key cannot be found according to the rules described in Accessor Search Patterns, then the object sends itself a valueForUndefinedKey: message. The default implementation of valueForUndefinedKey: raises an NSUndefinedKeyException, but subclasses may override this behavior and handle the situation more gracefully.

【译】返回由key参数命名的属性的值。如果根据访问者搜索模式中描述的规则找不到由键命名的属性,则该对象向自身发送一条valueForUndefinedKey:消息。的默认实现valueForUndefinedKey:会引发一个NSUndefinedKeyException,但是子类可以覆盖此行为,并更优雅地处理该情况。

  1. valueForKeyPath:setValue:ForKeyPath:Storyboardxib 中使用 KVC

Storyboardxib中使用KeyPath来修改控件的某个属性,如下图:

View
layer.cornerRadius进行修改,实现圆角,其实本质就是调用valueForKeyPath:setValue:ForKeyPath:

开发中不建议使用这种方式在 Storyboard 或者 xib 中修改,会造成后人很难找到很难维护
  • valueForKeyPathReturns the value for the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key—that is, for which the default implementation of valueForKey: cannot find an accessor method—receives a valueForUndefinedKey: message.

【译】valueForKeyPath: 返回相对于接收者的指定密钥路径的值。密钥路径序列中不符合特定键的键值编码的任何对象(即,默认实现valueForKey:无法找到访问器方法)均会接收到valueForUndefinedKey:消息。

  • setValue:forKeyPathSets the given value at the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key receives a setValue:forUndefinedKey: message.

setValue:forKeyPath:在相对于接收器的指定键路径处设置给定值。密钥路径序列中不符合特定键的键值编码的任何对象都会收到一条setValue:forUndefinedKey:消息。

通过valueForKeyPathsetValue:forKeyPath,可以对具有自己数据的可变对象设值,如下示例:

    LGPerson *person = [[LGPerson alloc] init];
    LGStudent *student = [[LGStudent alloc] init];
    student.subject    = @"iOSer";
    person.student     = student;
    [person setValue:@"我最帅" forKeyPath:@"student.subject"];
    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
  1. dictionaryWithValuesForKeys:setValuesForKeysWithDictionary:

查看官方文档

  • dictionaryWithValuesForKeysReturns the values for an array of keys relative to the receiver. The method calls valueForKey: for each key in the array. The returned NSDictionary contains values for all the keys in the array.

【译】返回相对于接收者的键数组的值。该方法调用 valueForKey:数组中的每个键。返回的NSDictionary值包含数组中所有键的值。

  • setValuesForKeysWithDictionarySets the properties of the receiver with the values in the specified dictionary, using the dictionary keys to identify the properties. The default implementation invokes setValue:forKey: for each key-value pair, substituting nil for NSNull objects as required.

【译】使用字典键识别属性,以指定字典中的值设置接收器的属性。默认实现setValue:forKey:为每个键值对调用,设置时将nil替换为NSNull

    NSDictionary* dict = @{
                           @"name":@"CC",
                           @"nick":@"KC",
                           @"subject":@"iOS",
                           @"age":@18,
                           @"length":@180
                           };
    LGStudent *p = [[LGStudent alloc] init];
    // 字典转模型
    [p setValuesForKeysWithDictionary:dict];
    NSLog(@"%@",p);
    // 键数组转模型到字典
    NSArray *array = @[@"name",@"age"];
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
    NSLog(@"%@",dic);

需要注意的是:集合对象,如NSArrayNSSetNSDictionary,不能包含nil的值。而是nil使用NSNull对象表示值。NSNull提供一个代表nil对象属性值的实例。
实现dictionaryWithValuesForKeys:setValuesForKeysWithDictionary:会自动在NSNull(在dictionary参数中)和nil(在存储属性中)之间相互转换。

2.2  访问集合属性

    NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
    ma[0] = @"100";
    NSLog(@"%@",[person valueForKey:@"array"]);

2.3  集合运算符

在使用valueForKeyPath:发送消息时,可以在键路径中嵌入集合运算符

路径格式:

1.left key path:指向的要进行运算的集合,如果是直接给集合发送的 valueForKeyPath: 消息,left 
key path 可以省略
2.right key path:指定了运算符应在集合中进行操作的属性。除之外,所有集合运算符都@count需要正确的密钥路径

集合运算符可以分为三种:

  • 聚合运算符

    • @avg 将指定的属性将其转换为double(用0代替nil值),并计算这些值的算术平均值。然后,以NSNumber形式返回
    NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
    
    • @count 返回操作对象指定属性的个数
    NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
    
    • @max 返回指定属性的最大值
    NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
    
    • @min 返回指定属性的最小值
    NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
    
    • @sum 返回指定属性的之和
    NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
    
  • 数组运算符

    • @distinctUnionOfObjects 返回操作对象指定属性的集合–去重
    NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
    
    • @unionOfObjects 返回操作对象指定的属性的集合
    NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
    
  • 嵌套运算符

    • @distinctUnionOfArrays 返回一个数组,该数组包含操作对象(嵌套集合)指定属性–去重
    • @unionOfArrays 返回一个数组,该数组包含操作对象指定的属性,不去重。
    • @distinctUnionOfSets 交集 返回一个NSSet,该NSSet包含操作对象(嵌套集合)指定属性–去重

2.4  访问非对象属性

非对像属性包含两种形式:基本数据类型,结构体(struct)

  • 访问基本数据类型


如图:常用的基本数据类型需要在设置属性的时候包装成 NSNumber类型,然后在读取值的时候使用各自对应的读取方法,

如: double 类型的标量读取的时候使用 doubleValue
  • 结构体

如图所示:NSPointNSRangeNSRectNSSize需要转换成 NSValue 类型,对于自定义的结构体,也需要进行 NSValue 的转换操作。如下示例:

typedef struct {
    float x, y, z;
} ThreeFloats;
 
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end


NSValue* result = [myClass valueForKey:@"threeFloats"];
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);

ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];

2.5  属性验证

调用validateValue:forKey:error:方法或者alidateValue:forKeyPath:error:方法进行属性验证。

会在接收验证消息的对象中查找与之匹配的validate<Key>:error:方法,如果没有这个方法,则验证成功,返回YES。当存在特定于属性的验证方法时,默认实现将返回调用该方法的结果。

简单的说,就是防止对错误的key赋值,也可以判断特定的key,进行重定向为另一个key

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
    if([inKey isEqualToString:@"name"]){
        [self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:inKey];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:10088 userInfo:nil];
    return NO;
}

2.6  KVC 取值和赋值原理

  • 赋值原理

    1. 依次判断是否有set<Key>或者_set<Key>或者setIs<key>的方法。如果找到,直接调用,没有,则进入第 2 步。
    2. 判断accessInstanceVariablesDirectly 方法,是否开启实例变量赋值,若返回YES,则进入步骤3,返回NO,则进去步骤4
    3. 依次寻找类似于_<key>_is<Key><key>或者is<Key>的实例变量。如果找到,直接设置变量。找不到,则进去步骤4
    4. 找不到访问器或实例变量后,调用setValue:forUndefinedKey:,报错,引发异常。
  • 取值原理

    1. 依次判断是否有访问方法get<Key><key>is<Key>或者_<key>,如果找到,则执行步骤 6进行细节处理。否则,请继续下一步。

    2. 判断是否是NSArray

    3. 判断是否是NSSet

    4. 判断accessInstanceVariablesDirectly 方法,是否开启实例变量赋值,若返回YES,则进入步骤5,返回NO,则进去步骤7

    5. 依次查找名为_<key>_is<Key><key>或者is<Key>的实例变量,如果找到,直接获取值,找不到则直接执行步骤 6

    6. 如果检索到的属性值是对象指针,则只需返回结果,

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

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

    7. 调用valueForUndefinedKey:,报错,引发异常

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章