【底层探索】- KVC原理分析


思考拓展题

为什么先进入objc_setProperty_atomic_copy 方法,而不是setName:?

回答:

通用原则。即面向不同的数据类型,采用统一的属性设值的方法,会更加的高效,符合高内聚的原则。

一、概念及常见使用

1.0 概念

KVC(Key-Value coding) 是一个由NSKeyValueCoding 协议触发的机制,允许对象通过该机制获取他们的属性。当一个对象符合KVC 原则,它的属性就可以用字符串通过一个简洁、相同的信息接口快捷访问到。

这种简洁访问机制,借助实例变量量和关联存取方法补充了对象直接访问的机制。

相关链接:关于KVC(Apple Opensource)

1.1 简单使用

假设有类BankAccount,有众多属性如下:

@interface BankAccount : NSObject

@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation

@end

给起通过KVC属性赋值,如下

[myAccount setValue:@(100.0) forKey:@"currentBalance"];

1.2 集合类型

对数组进行修改:

person.array = [@"1", @"2", @"3"];
  • 方法一、创建新的数组(普通方法)
    NSArray *array = [person valueForKey:@"array"];
    // 用 array 的值创建一个新的数组
    array = @[@"100",@"2",@"3"];
    [person setValue:array forKey:@"array"];
  • 方法二、通过可变数组赋值(KVC)
    NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
    ma[0] = @"100";

可见可变数组实行KVC 方法,更为简洁

1.3 集合操作符

集合操作符如下:

比如某个模型结构如下:

@interface Transaction : NSObject

@property (nonatomic) NSString* payee;   // To whom
@property (nonatomic) NSNumber* amount;  // How much
@property (nonatomic) NSDate* date;      // When

@end

想要获取多个Transactionpayee 属性的结合,可以用集合运算符

NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

可见,集合运算符主要在于获取对象的值

1.4 访问非对象属性

问题:如何访问结构体?

这个问题经常出现在C、C++混编的环境下出现,解决的防范也比较简单,由于结构体并不遵循Key-Value Coding,可以先将其转为遵循KVC的NSValue 类型,然后通过KVC 对其进行存取。

给一个结构体ThreeFloats,作为Person 的属性出现

typedef struct {
    float x, y, z;
} ThreeFloats;

@interface Person : NSObject{}

@property (nonatomic)         ThreeFloats       threeFloats;

@end
  • 转换为NSValue

        ThreeFloats floats = {1., 2., 3.};
        NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
  • 存其值

        [person setValue:value forKey:@"threeFloats"];
        NSValue *reslut = [person valueForKey:@"threeFloats"];
  • 取其值

        ThreeFloats th;
        [reslut getValue:&th] ;
        NSLog(@"%f - %f - %f",th.x,th.y,th.z);

打印结果如下:

1.5 通过keyPath 实现多级访问

这种情况出现在多级对象属性的情况下,如下面:

@interface Person : NSObject{
    @property (nonatomic, strong) LGStudent         *student;
}

@interface Student : NSObject
    @property (nonatomic, copy)   NSString          *name;
}
    Student *student = [[Student alloc] init];
    student.subject    = @"iOS";
    person.student     = student;
  • 存值

    通过keyPath 设置值的方式如下:

    [person setValue:@"大师班" forKeyPath:@"student.subject"];

    这里用到了student.subject 实现多级别访问

  • 取值

    也通过student.subject 实现多级别访问

        NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

二、KVC 原理剖析

2.1 分析Setter 调用过程

对对象对实例变量赋值的流程如下:

  1. 查找setter 方法,如果存在,赋值成功
  2. 查看 accessInstanceVariablesDirectly (直接访问成员遍历)是否开启
    1. 若开启,查找 _, _is, , or is
    2. 若未开启,直接给变量赋值
  3. 若以上都没执行,会报错,结果位 setValue:forUndefinedKey

2.2 分析Getter 取值过程

  1. 先查找Getter 方法,如果存在,取值成功。

    采用的是标准的getter 方法

    get<Key>, <key>, is<Key>, or _<key>
  2. 如果非集合类型:查看 accessInstanceVariablesDirectly (直接访问成员遍历)是否开启

    1. 如果开启——通过 _, _is, , or is 查找实现取值
    2. 如果关闭——其他查找
  3. 如果没有,直接赋值

  4. 报错 valueForUndefinedKey

三、自定义KVC 方法

上面理解了KVC 的设值与取值过程,尝试自定义一个KVC。其实分析完上面的设值与取值,仿造的过程也是依样画葫芦。具体步骤如下:

3.1 Setter 方法

  1. 创建一个类的分类。在分类里创建取值和赋值的方法,有如下方法

    - (void)lg_setValue:(nullable id)value forKey:(NSString *)key;
  2. 判断key 的有效性

    if(!key) || key.length == 0{
       return
    }
  3. 找到相应的的 set、 _set、setIs 方法,主要看本类是否相应对应名字的方法,实现如下:

       NSString *Key = key.capitalizedString;
       // 拼接方法
       NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
       NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
       NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
       if ([self lg_performSelectorWithMethodName:setKey value:value]) {
           NSLog(@"*********%@**********",setKey);
           return;
       }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
           NSLog(@"*********%@**********",_setKey);
           return;
       }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
           NSLog(@"*********%@**********",setIsKey);
           return;
       }

    看是否响应该方法

    - (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
    
       if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
    
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
           [self performSelector:NSSelectorFromString(methodName) withObject:value];
    #pragma clang diagnostic pop
           return YES;
       }
       return NO;
    }
  4. 判断是否能直接赋值实例变量

    1. 这是一个重要的开关.开启,则可以通过手动设值,跳到5
    2. 如果关闭,则由于本类不相应对应的setter 方法,无法有效赋值,会进行异常抛出如下:
    if (![self.class accessInstanceVariablesDirectly] ) {
            @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
        }
    
  5. 找到相应的实例变量进行复制

    1. 定义一个收集实例变量的的可变数组

          NSMutableArray *mArray = [self getIvarListName];
          // _<key> _is<Key> <key> is<Key>
          NSString *_key = [NSString stringWithFormat:@"_%@",key];
          NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
          NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    2. 获取相应的 ivar

      if ([mArray containsObject:_key]) {
              // 4.2 获取相应的 ivar
             Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
      }
    3. 对相应的ivar 设置值

      // 4.3 对相应的 ivar 设置值
      object_setIvar(self , ivar, value);
      return;
  1. 找不到相应的实例——报错处理

    @throw [NSException exceptionWithName:@"UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];

3.2 Getter 方法

getter 方法与 Setter 方法相仿,继续进行一下

  1. 设置自定义的getter 方法。代码如下:

    - (id)lj_valueForKey: (NSString *)key;
  2. 判断key,保证不为空

    if (key == nil  || key.length == 0) 
       return nil;
  3. 找到对应的get、countOf 、objectInAtIndex 等直接取值的方法。后两种一般是集合类型(数组等)用到的方法。

    1. 先对键Key 进行大写转换

      // key 要大写
      NSString *Key = key.capitalizedString;
    2. 用get、countOf、objectIn 等方法,对key拼接

          // 拼接方法
      NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
      NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
      NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
    3. 判断本类是否相应对应的get、countOf、objectInAtIndex 方法,如果有,就执行该方法,直接获取到对应的值。

      #pragma clang diagnostic push
      #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
              return [self performSelector:NSSelectorFromString(getKey)];
          }else if ([self respondsToSelector:NSSelectorFromString(key)]){
              return [self performSelector:NSSelectorFromString(key)];
          }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
              if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
                  int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
                  NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
                  for (int i = 0; i<num-1; i++) {
                      num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
                  }
                  for (int j = 0; j<num; j++) {
                      id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                      [mArray addObject:objc];
                  }
                  return mArray;
              }
          }
      #pragma clang diagnostic pop
  4. 此时判断能否直接赋值实例变量。用到的是该类的accessInstanceVariablesDirectly 属性是否开启。

    • 如果已打开,则表示可以通过取其 _、_is、is 来取值,跳到5

    • 若未打开,抛出异常,表示找不到该键对应值

      相关代码如下:

      if (![self.class accessInstanceVariablesDirectly] ) {
              @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
          }
  5. 找到实例变量,进行取值

    • 定义一个收集实例变量的可变数组:

          NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
          unsigned int count = 0;
          Ivar *ivars = class_copyIvarList([self class], &count);
          for (int i = 0; i<count; i++) {
              Ivar ivar = ivars[i];
              const char *ivarNameChar = ivar_getName(ivar);
              NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
              NSLog(@"ivarName == %@",ivarName);
              [mArray addObject:ivarName];
          }
          free(ivars);
    • 依次判断该数组是否包含键关键字,其顺序为 _key——> _isKey——> key——> isKey,通过找到的关键字,进行取值

          NSString *_key = [NSString stringWithFormat:@"_%@",key];
          NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
          NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
          if ([mArray containsObject:_key]) {
              Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
              return object_getIvar(self, ivar);;
          }else if ([mArray containsObject:_isKey]) {
              Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
              return object_getIvar(self, ivar);;
          }else if ([mArray containsObject:key]) {
              Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
              return object_getIvar(self, ivar);;
          }else if ([mArray containsObject:isKey]) {
              Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
              return object_getIvar(self, ivar);;
          }

四、KVC 实用小技巧

4.1 自动转换类型

KVC 具备自动转换数据类型的功能

当我们对Int 类型的属性,添加字符串的类型,正常是这样的

@interface LGPerson : NSObject 
@property (nonatomic, assign) int  age;
@end

[person setValue:@18 forKey:@"age"];

那么下面这种方式能成功吗?

[person setValue:@"20" forKey:@"age"]; 

可见是可以执行的,在通过KVC 设置之后,自动转换成了int类型,整个数据成为了NSCFNumber。可见其灵活程度!

4.2 空值报警

总有些时候,设置值可能会异常为空,那怎么办呢?可以通过重写空值报警来避免异常

  • 设值为空重写

    - (void)setNilValueForKey:(NSString *)key{
        NSLog(@"报告! 设置 %@ 是空值",key);
    }
  • 取值为空重写

    - (id)valueForUndefinedKey:(NSString *)key{
        NSLog(@"报告!!!: %@ 没有这个key - 给你一个其他的吧,别奔溃了!",key);
        return @"Master 牛逼";
    }

4.3 键值验证

当类对应的键的值还是找不到,即将要报错了,我们还是可以自行挽救一下——先验证对应的键值是否成功,如果不成功,自行报错并写补救方法。

NSError *error;
    NSString *name = @"Good";
    if (![person validateValue:&name forKey:@"names" error:&error]) {
        NSLog(@"%@",error);
    }else{
        NSLog(@"%@",[person valueForKey:@"name"]);
    }

实现方法里,可以这样写

//MARK: - 键值验证 - 容错 - 派发 - 消息转发

- (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;
}

五、总结

总的来说,KVC 是比较简单的一个章节,其原理是对类内部逐级查验,从set 方法、直接赋值开关、isKey 等键的取值实现赋值和取值的过程。而实现自定义KVC,则是依照逻辑一步一步写成。


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
【数据结构与算法】-(3)循环链表(单向) 【数据结构与算法】-(3)循环链表(单向)
【数据结构与算法】-(1)基础篇 【数据结构与算法】-(2)线性表基础 【数据结构与算法】-(3)循环链表(单向) 【数据结构与算法】-(4)双向链表和双向循环链表 【数据结构与算法】-(5)链表面试题解析 【数据结构与算法】-(6)栈
2020-04-02
下一篇 
【数据结构与算法】-(2)线性表基础 【数据结构与算法】-(2)线性表基础
【数据结构与算法】-(1)基础篇 【数据结构与算法】-(2)线性表基础 【数据结构与算法】-(3)循环链表(单向) 【数据结构与算法】-(4)双向链表和双向循环链表 【数据结构与算法】-(5)链表面试题解析 【数据结构与算法】
2020-04-01 李佳
  目录