iOS 深入了解 KVC 本质

321 阅读9分钟

KVC 的全称是 Key-Vlaue Coding,中文全称键值编码。它是一种可以直接通过字符串的名字(Key)来访问类属性(实例变量)的机制。

KVC 的相关 API

常用方法

  • 通过属性名字作为key设值/取值
// 直接通过属性名字来取值
- (nullable id)valueForKey:(NSString *)key;
// 直接通过属性名字来设置
- (void)setValue:(nullable id)value forKey:(NSString *)key;
  • 通过keyPath(即路由)设值/取值
// 通过路由的方式来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; 
// 通过路由的方式来设值                 
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

其他方法

// 默认返回 YES,表示如果没有找到Set<Key>方法的话,会按照 _key,_iskey,key,iskey的顺序搜索成员,设置成 NO 就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;

// KVC 提供属性值正确性验证的 API,它可以用来检查 set 的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

// 如果属性是一个 NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 如果属性是一个 NSSet,那么可以用这个方法来返回。
- (NSMutableSet *)mutableSetValueForKey:(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;

KVC 设置的底层流程

针对对象属性的赋值,一般有以下两种方式

  • 直接通过setter方法赋值
  • 通过KVC键值编码的相关API赋值
LGPerson *person = [[LGPerson alloc] init];
// 1、一般 setter 方法
person.name      = @"Justin";
// 2、KVC 方式
[person setValue:@"Justin" forKey:@"name"]; 

下面针对 KVC 设值方法:setValue:forKey,来进行其底层原理的探索

首先进入setValue:forKey的声明,发现是在Foundation框架中,而Foundation框架是不开源的,有以下几种方式可以去探索底层

  • 通过Hopper反汇编,查看伪代码(不推荐,太麻烦)
  • 通过苹果官方文档(推荐)
  • Github搜索是否有相关的 Demo(可以参考 DIS_KVC_KVO 项目)

我们通过Key-Value Coding Programming Guide苹果官方文档来分析 KVC 设值流程。当调用setValue:forKey:设置属性value时,其底层的执行流程:

  • 【第一步】首先查找是否有这三种setter方法,按照查找顺序为set<Key>:-> _set<Key> -> setIs<Key>
  1. 如果有其中任意一个setter方法,则直接设置属性的value
  2. 如果都没有,则进入【第二步】
  • 【第二步】:如果没有找到第一步中的三个 setter 方法,则查找accessInstanceVariablesDirectly是否返回YES
  1. 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:_<key> -> _is<Key> -> <key> -> is<Key>
  2. 如果找到其中任意一个实例变量,则赋值
  3. 如果都没有,或返回 NO,则进入【第三步】
  • 【第三步】如果 setter 方法或实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

image.png

KVC 取值的底层流程

同样的我们可以通过官方文档分析 KVC 取值流程。当调用valueForKey:时,其底层的执行流程:

  • 【第一步】首先查找 getter 方法,按照get<Key> -> <key> -> is<Key> -> _<key>的方法顺序查找,

    1. 如果找到,则进入【第五步】
    2. 如果没有找到,则进入【第二步】
  • 【第二步】如果第一步中的 getter 方法没有找到,KVC 会查找countOf <Key>和objectIn <Key> AtIndex :和<key> AtIndexes :

    1. 如果找到countOf <Key>和其他两个中的一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即NSKeyValueArray,是NSArray子类。代理对象随后将接收到的所有NSArray消息转换为countOf<Key>,objectIn<Key> AtIndex:和<key>AtIndexes:消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get<Key>:range:之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)
    2. 如果没有找到这三个访问数组的,请继续进入【第三步】
  • 【第三步】如果没有找到上面的几种方法,则会同时查找countOf <Key>,enumeratorOf<Key>和memberOf<Key>这三个方法

    1. 如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOf<Key>,enumeratorOf<Key>和memberOf<Key>:消息的某种组合,用于创建它的对象
    2. 如果还是没有找到,则进入【第四步】
  • 【第四步】如果还没有找到,检查类方法accessInstanceVariablesDirectly是否YES,依次搜索_<key>,_is<Key>,<key>或is<Key>的实例变量
    如果搜到,直接获取实例变量的值,进入【第五步】

  • 【第五步】根据搜索到的属性值的类型,返回不同的结果。

    1. 如果是对象指针,则直接返回结果
    2. 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它。
    3. 如果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。
  • 【第六步】如果上面5步的方法均失败,系统会执行该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常。

image.png

自定义 KVC

自定义 KVC 只是为了让我们更好的理解 KVC 底层执行流程,实际开发中并不需要自定义 KVC。

自定义 KVC 设值

自定义 KVC 设置流程:

  1. 判断Key非空

  2. 查找setter方法,顺序是:setXXX、_setXXX、 setIsXXX

  3. 判断是否响应accessInstanceVariablesDirectly方法,即间接访问实例变量。

    • 返回YES,继续下一步设值。
    • 如果是NO,则崩溃。
  4. 间接访问变量赋值(只会走一次),顺序是:_key、_isKey、key、isKey

    • 4.1 定义一个收集实例变量的可变数组。
    • 4.2 通过class_getInstanceVariable方法,获取相应的 ivar。
    • 4.3 通过object_setIvar方法,对相应的 ivar 设置值。
  5. 如果找不到相关实例变量,则抛出异常。

完成代码如下:

// 设值
- (void)cjl_setValue:(nullable id)value forKey:(NSString *)key {
//  1、判断 key 是否存在
    if (key == nil || key.length == 0) return;
    
//  2、找 setter 方法,顺序是:setXXX、_setXXX、 setIsXXX
    // 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 cjl_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*************%@*************", setKey);
        return;
    }else if([self cjl_performSelectorWithMethodName:_setKey value:value]){
        NSLog(@"*************%@*************", _setKey);
        return;
    }else if([self cjl_performSelectorWithMethodName:setIsKey value:value]){
        NSLog(@"*************%@*************", setIsKey);
        return;
    }
//  3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"CJLUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
//  4、间接访问变量赋值,顺序为:_key、_isKey、key、isKey
//  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:@"MPUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    
}

自定义 KVC 取值

自定义 KVC 取值流程:

  1. 判断key非空

  2. 查找相应方法,顺序是:get<Key>、 <key>、 countOf<Key>、 objectIn<Key>AtIndex

  3. 判断是否能够直接赋值实例变量,即判断是否响应accessInstanceVariablesDirectly方法,间接访问实例变量。

    • 返回YES,继续下一步取值
    • 如果是NO,则崩溃
  4. 间接访问实例变量,顺序是:_<key> _is<Key> <key> is<Key>
    4.1 定义一个收集实例变量的可变数组
    4.2 通过class_getInstanceVariable方法,获取相应的 ivar。
    4.3 通过object_getIvar方法,返回相应的 ivar 的值。

完成代码如下:

//取值
- (nullable id)cjl_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、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回 YES,继续下一步设值,如果是 NO,则崩溃
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"MPUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
//  4.找相关实例变量进行赋值,顺序为:_<key>、 _is<Key>、 <key>、 is<Key>
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // 例如:_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 @"";
}

路由的方式访问

示例代码如下

// MPPerson 类
@interface MPPerson : NSObject
@property (nonatomic, strong) MPStudent        *student;
@end

// MPStudent 类
@interface MPStudent : NSObject
@property (nonatomic, copy)   NSString          *name;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MPPerson *person = [[MPPerson alloc] init];
        MPStudent *student = [MPStudent alloc];
        student.name    = @"Justin";
        person.student     = student;
        //根据kvc - keyPath路由修改student的subject属性的值
        [person setValue:@"改行" forKeyPath:@"student.name"];
        NSLog(@"%@",[person valueForKeyPath:@"student.name"]);
    }
    return 0;
}

访问非对象属性

// 结构体
typedef struct {
    float x, y, z;
} ThreeFloats;

// MPPerson 类
@interface MPPerson : NSObject
@property (nonatomic) ThreeFloats threeFloats;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 结构体
        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);
    }
    return 0;
}

KVC 应用场景

  1. 访问和修改私有变量。
  2. Model 和字典转换(参考 YYModel 源码)。
4. 修改系统内部的属性
// 字典转模型
-(void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
// 模型转字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
  1. 修改系统控件内部的属性(比如 UITextField 的 placeHolderText 属性)。
  2. KVC实现消息传递。
- (void)arrayMessagePass {
    NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
    NSArray *lenStr= [array valueForKeyPath:@"length"];
    NSLog(@"%@",lenStr);// 消息从array传递给了string
    NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
    NSLog(@"%@",lowStr);
}