底层学习--KVC

536 阅读7分钟

KVC是什么?

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

KVC在Objective-C中的定义:KVC的定义都是对NSObject的,扩展来实现的(Objective-C中有个显式的 NSKeyValueCoding类别名-分类)。 查看setValueForKey方法,发现其在Foundation里面,而Foundation框架是不开源的,只能在苹果官方文档查找。见下图:

image.png

KVC提供的API

我们可以学习解读苹果的官方文档,对KVC有更深的理解。
Key-Value Coding Programming Guide
苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。

常用方法

对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC,下面是KVC最为重要的四个方法:

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

特殊方法

NSKeyValueCoding类别中还有其他的一些方法,这些方法在碰到特殊情况或者有特殊需求还是会用到的。

// 默认返回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<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

特殊数据处理

有时候一些特殊的数据需要我们做value处理,如struct,字典等

结构体处理

KVC在进行结构体处理时,需要用到NSValue,设值时,将结构体封装成NSValue,进行键值设值;取值同样返回NSValue,然后按照结构体格式进行解析,见下面代码:

// 结构体    
struct ThreeFloats {
    CGFloat x,
    CGFloat y,
    CGFloat z
};
struct ThreeFloats floats = {1.0, 2.0, 3.0};    
// 封装成NSValue    
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];    
// 设值    
[person setValue:value forKey:@"threeFloats"];
// 取值    
NSValue *value1 = [person valueForKey:@"threeFloats"];    
// 结构体解析   
ThreeFloats th;    
[value1 getValue:&th];    
NSLog(@"%f-%f-%f",th.x,th.y,th.z);

字典处理

//字典转模型
NSDictionary *dic = @{@"name" : @"张三", @"nick" : @"ZS",
                      @"age" : @(18), @"height" : @(180.5)};
Person *p = [[Person alloc] init];
[p setValuesForKeysWithDictionary:dic];

// 模型转字典
NSArray *array = @[@"name", @"nick", @"age", @"height"];
NSDictionary *dic1 = [p dictionaryWithValuesForKeys:array];
NSLog(@"dic1 = %@",dic1);

KVC 设值 & 取值

KVC是怎么使用的,我们都很清楚,那么KVC在内部是按什么样的顺序来寻找key的呢?这是我们要探索的重点。

设值

当调用setValue:forKey:代码时,底层的执行机制是怎样的呢?在官方文档中有相关的说明,见下图: image.png setValue: forkey:的默认实现,给定key和value参数作为输入,尝试将key的属性设置为value,在接受调用的对象内部,使用一下过程:

  1. 按顺序查找名称为 set<Key>_set<Key>的方法,如果找到,则调用该方法
  2. 如果未找到以上方法,判断accessInstanceVariblesDirectionly方法是否返回YES
  3. - 如果为YES,则按顺序查找名称为 `_<key>`, `_is<Key>`, `<key>`, `is<isKey>`的实例变量   
        a:如果找到,直接使用输入值设置变量并完成   
        b:如果未找到,则调用 `setValue: forUndefinedKey:`
    
  4. - 如果为NO,则调用 `setValue: forUndefinedKey:`
    

取值

当调用valueForKey:的代码时,底层的执行机制又是怎样的呢?在官方文档中有相关的说明,见下图: image.png

  1. 按照get<Key><key>is<key>_<key>的顺序查找getter方法,找到的话会直接调用,如果是BOOL或者int等值类型,会将其宝成一个NSNumber对象
  2. getter方法没有找到,KVC则会查countOf<Key>objectIn<Key>AtIndex或者<Key>AtIndex格式的方法,如果找到就会返回一个可以相应的NSArray。
  3. 如果上面的方法没有找到,那么会同时查找countOf<Key>enumeratorOf<Key>memberOf<Key>格式的方法,如果这三个方法都找到,name就返回一个可以响应NSSet的方法代理集合
  4. 上面方法没有找到,就会判断accessInstanceVariblesDirectionly是否返回YES,
    • 如果返回YES,则按顺序获取成员变量的值,_<key>_is<Key><key>is<Key>
    • 如果返回NO,则调用valueForUndefinedKey:方法

验证流程

在调用setter 和 getter时,按顺序注释掉方法,就能发现设值 和 取值的访问顺序 image.png

在KVC中使用keyPath

除了对当前对象的属性进行赋值外,还可以对其更深层的对象进行赋值。例如,对当前对象的dog属性的name属性进行赋值。KVC进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。

[person setValue:@"lucky" forKeyPath:@"dog.name"];

通过keyPath对数组进行取值时,并且数组中存储的对象类型都相同,可以通过valueForKeyPath:方法指定取出数组中所有对象的某个字段。例如下面例子中,通过valueForKeyPath:将数组中所有对象的name属性值取出,并放入一个数组中返回。

NSArray *names = [array valueForKeyPath:@"name"];

image.png

异常处理

当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个异常,并且应用程序Crash。见下图: image.png 我们可以重写下面两个方法:

- (id)valueForUndefinedKey:(NSString *)key {        
    NSLog(@"出现异常,该key不存在%@",key);        
    return nil;    
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {   
    NSLog(@"出现异常,该key不存在%@",key);   
}

自定义KVC

#import "NSObject+KVC.h"

#import <objc/runtime.h>

\


@implementation NSObject (KVC)

- (void)zy_setValue:(id)value forKey:(NSString *)key {
    if (key.length == 0) {
        return;
    }
    
    // 查找setter方法并调用setKey: _setKey  setIsKey:
    NSString *Key = key.capitalizedString;
    NSString *setKey = [NSString stringWithFormat:@"set:%@",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set:%@",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs:%@",Key];

    if ([self zy_performSELWithName:setKey object:value]) {
        return;
    }

    if ([self zy_performSELWithName:_setKey object:value]) {
        return;
    }

    if ([self zy_performSELWithName:setIsKey object:value]) {
        return;
    }
    
    //判断accessInstanceVariablesDirectly是否允许获取实例变量
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }  

    //判断是否存在响应的变量,对变量赋值 _key _isKey key isKey
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",key.capitalizedString];
    NSString *isKey = [NSString stringWithFormat:@"is%@",key.capitalizedString];
    
    NSArray *ivarList = [self getIvarList];
    if ([ivarList containsObject:_key]) {
        [self setIvarWithKey:_key value:value];
        return;
    }

    if ([ivarList containsObject:_isKey]) {
        [self setIvarWithKey:_isKey value:value];
        return;
    }

    if ([ivarList containsObject:key]) {
        [self setIvarWithKey:key value:value];
        return;
    }
    
    if ([ivarList containsObject:isKey]) {
        [self setIvarWithKey:isKey value:value];
        return;
    }

    // 如果找不到相关实例
    @throw [NSException exceptionWithName:@"UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

- (id)zy_valueForKey:(NSString *)key {
    if (key.length == 0) {
        return nil;
    }

    // 找到相关方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    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
    // 判断是否能够直接赋值实例变量-YES、NO
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }

    // 找相关实例变量进行赋值 _<key> _is<Key> <key> is<Key>
    NSArray *ivarList = [self getIvarList];
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];

    // 判断是否存在对应的成员变量
    if ([ivarList containsObject:_key]) {
        return [self getIvarWithKey:_key];
    } else if ([ivarList containsObject:_isKey]) {
        return [self getIvarWithKey:_isKey];
    } else if ([ivarList containsObject:key]) {
        return [self getIvarWithKey:key];
    } else if ([ivarList containsObject:isKey]) {
        return [self getIvarWithKey:isKey];
    }
    return nil;
}

- (BOOL)zy_performSELWithName:(NSString *)name object:(id)object {
    if ([self respondsToSelector:NSSelectorFromString(name)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(name) withObject:object];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (NSArray *)getIvarList {
    NSMutableArray *arr = [NSMutableArray array];
    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];
        [arr addObject:ivarName];
    }
    free(ivars);
    return arr;
}

- (void)setIvarWithKey:(NSString *)key value:(id)value {
    Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
    object_setIvar(self, ivar, value);
}

- (id)getIvarWithKey:(NSString *)key {
    Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
    return object_getIvar(self, ivar);
}
@end