iOS底层-KVC

1,001 阅读6分钟

KVC的全拼是Key-Value Coding,中文是键值编码。是由NSKeyValueCoding非正式协议的一种机制。对象可以间接地访问它们的属性。这种间接访问机制是实例变量及其相关访问器方法提供的直接访问的补充。

使用KVC

通过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; 

我们平时项目中主要是通过使用的是valueForKeyvalueForKeyPath的方式取值和设值,当然还有集合类型的一些操作可以参见苹果KVC的文档查看使用。

KVC的设值过程-

setValue:forKey:这个方法有一个调用顺序(基本数据类型):

  • 按顺序查找名为 set: 或 _set 的第一个访问器。如果找到,则使用输入值(或根据需要展开的值)调用它并完成。
  • 如果未找到简单访问器,并且类方法 accessInstanceVariablesDirectly 返回 YES,则按顺序查找名称类似于 _、_is、 或 is 的实例变量。如果找到,直接使用输入值(或解包值)设置变量并完成。
  • 在未找到访问器或实例变量时,调用 setValue:forUndefinedKey:。默认情况下,这会引发异常,但 NSObject 的子类可能会提供特定于键的行为。 整个流程图如下(以person对象设置name属性为例):

kvc设值流程.png

KVC取值过程

和设值过程一样,取值过程valueForKey:也有一个调用顺序(包含集合类型):

  • 在实例中搜索找到的第一个访问器方法,其名称类似于 get、、is 或 _,按该顺序。如果找到,则调用它并使用结果继续执行步骤 5。否则继续下一个步骤
  • 如果没有找到简单的访问器方法,则在实例中搜索名称与模式 countOf 和 objectInAtIndex:(对应于 NSArray 类定义的原始方法)和 AtIndexes:(对应于模式)的方法NSArray 方法 objectsAtIndexes:)。如果找到其中的第一个和至少其他两个中的一个,则创建一个集合代理对象,该对象响应所有 NSArray 方法并返回该对象。否则,继续执行步骤 3。代理对象随后将它接收到的任何 NSArray 消息转换为 countOf、objectInAtIndex: 和 AtIndexes: 消息的某种组合,并将其转换为创建它的键值编码兼容对象。如果原始对象还实现了一个可选方法,其名称类似于 get:range:,则代理对象也会在适当的时候使用它。实际上,代理对象与键值编码兼容对象一起工作允许底层属性表现得好像它是一个 NSArray,即使它不是。
  • 如果没有找到简单的访问器方法或数组访问方法组,则查找名为 countOf、enumeratorOf 和 memberOf 的三元组方法:(对应于 NSSet 类定义的原始方法)。如果找到所有三个方法,则创建一个集合代理对象,该对象响应所有 NSSet 方法并返回该对象。否则,继续执行步骤 4。这个代理对象随后将它接收到的任何 NSSet 消息转换为 countOf、enumeratorOf 和 memberOf 的某种组合:消息到创建它的对象。实际上,与键值编码兼容的对象一起工作的代理对象允许底层属性表现得好像它是一个 NSSet,即使它不是。
  • 如果没有找到简单的访问器方法或集合访问方法组,并且如果接收者的类方法accessInstanceVariables直接返回YES,则搜索名为_、_is、或is的实例变量,以该顺序。如果找到,直接获取实例变量的值并进行步骤5,否则进行步骤6。
  • 如果检索到的属性值是一个对象指针,只需返回结果即可。 如果该值是 NSNumber 支持的标量类型,则将其存储在 NSNumber 实例中并返回。 如果结果是 NSNumber 不支持的标量类型,则转换为 NSValue 对象并返回。
  • 如果所有其他方法都失败,请调用 valueForUndefinedKey:。默认情况下,这会引发异常,但 NSObject 的子类可能会提供特定于键的行为。

其流程图如下:

KVC取值过程.png

自定义实现KVC

如果自己实现一个KVC可以参考上面的顺序,实现valueForKeysetValueForKey

设值过程

  • 判断key是否为空,为空直接返回。

  • 查找是否有 setter方法 set<Key>:_set<Key>, setIs<Key>,如果有则实现并返回。

  • 如果没找到则判断accessInstanceVariablesDirectly的返回值是否为YES,可以则往下走,否则抛出异常。

  • 查找自己的ivar列表中是否包含实例变量 _<key>, _is<Key>,<key>,is<Key>,找到就赋值。

  • 如果都搜索不到,就抛出异常。

实现代码

- (void)js_setValue:(nullable id)value forKey:(NSString *)key{
    // 空判断
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 2: setter set<Key>: or _set<Key>,
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self js_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self js_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self js_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"JSUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4: 间接变量
    // 获取 ivar -> 遍历 containsObjct -
    // 4.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];
    if ([mArray containsObject:_key]) {
        // 4.2 获取相应的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 对相应的 ivar 设置值
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }
    
    // 5:如果找不到相关实例
    @throw [NSException exceptionWithName:@"JSUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

取值过程

  • 同样是判断key非空

  • 按顺序查找方法:get<Key><key>countOf<Key>objectIn<Key>AtIndex

  • 判断accessInstanceVariablesDirectly的返回值是否为YES,可以则往下走,否则抛出异常。

  • 查找自己的ivar列表中是否包含实例变量 _<key>, _is<Key>,<key>,is<Key>,找到就取值。

  • 未找到抛出异常。

- (nullable id)js_valueForKey:(NSString *)key{
    
    // 1:判断非空
    if (key == nil  || key.length == 0) {
        return nil;
    }
    // 2:找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
        
#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
    
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"JSUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    // 4.找相关实例变量进行赋值
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _name -> _isName -> name -> isName
    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);;
    }
    return @"";
}

总结

本篇我们主要探索了KVC的取值设值的流程。设值过程集合类型的情况没有写,感兴趣的童鞋可以查看苹果官方文档进行探索。