KVC初探

193 阅读7分钟

下班随拍

在探索KVC之前我们先了解一下成员变量、实例变量、属性的区别,因为KVC的实现是建立在这些的基础上。

#成员变量、实例变量、属性

首先我们定义一个Person类:

@interface Person : NSObject {
	@public
	int age;
	NSString *name;
    id other;
}
@property (nonatomic, copy) UIButton *button;
  • 成员变量: 如上所示大括号里面全部为成员变量;
  • 实例变量: 由实例类初始化的对象为实例变量,如name就是成员变量,但是实例变量是一种特殊的成员变量; 但是这里idOC特有的一种Class,在这里不确定是不是成员变量。
  • 属性 : 由property 声明,默认生成setter getter方法。

这里的生成是由LLVM编译器生成,早期是GCC编译器,LLVM如果发现一个实例变量或者成员变量没有匹配到实例变量的属性的时候会自动创建一个带下划线的成员变量,并添加setter getter方法。


# 什么是KVC ![](https://upload-images.jianshu.io/upload_images/2936157-56d7f5c85ca91fd9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

概念:通过官方文档我们可以看到KVC是一种机制,通过keyPath间接访问成员变量,是一种键值编码,这里可以想到字典的赋值也是依赖于KVC实现。开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定。
KVC的常用方法: 下面这4个方法是我们比较常用的

 //直接通过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;  

现在我们知道了KVC的基本概念及常用的方法,那么我们就根据这几个方法来探究一下KVC取值跟赋值的内部机制是什么样的。

KVC取值过程

这里只讲简单的数据类型的取值过程,array set 类型先不讲 取值步骤1.png 取值步骤2.png 取值步骤3.png

  • 步骤1 结合文档的意思就是说当我们调用[person valueForKey:@"name"]这样一个方法来取值的时候,对象内部会按顺序依次查找get<key><key>is<key>_<key>这些方法有没有实现。具体是不是呢?我们就通过下面的代码来验证一下。
//通过valueForKey取值
Person  *p = [[Person alloc] init];
NSString *name = [p valueForKey:@"name"];

#pragma mark - getter相关
// 1
- (NSString *)getName {
    NSLog(@"%s",__func__);
    return NSStringFromSelector(_cmd);
}

// 2
- (NSString *)name {
    NSLog(@"%s",__func__);
    return NSStringFromSelector(_cmd);
}

// 3
- (NSString *)isName {
    NSLog(@"%s",__func__);
    return NSStringFromSelector(_cmd);
}

//4
- (NSString *)_name {
    NSLog(@"%s",__func__);
    return NSStringFromSelector(_cmd);
}

[p valueForKey:@"name"]取值打印结果.png

通过打印我们可以看出我们明明是通过name来取值,但是调用的确是- (NSString *)getName方法,当我们把- (NSString *)getName注释的时候就会调用- (NSString *)name方法,剩下的两个方法也是一样,这里就可以验证了步骤1的顺序。

  • 步骤2 当面的方法都没实现的时候,会先判断+ (BOOL)accessInstanceVariablesDirectly(是否开启间接访问,默认是开启,猜测是苹果这样做的目的是为了达到兼容性或者动态性)方法是否返回YES,如果返回YES,标识开启,就会依次去访问_<key>, _is<Key>, <key>, is<Key>这些成员变量。

一样我们来验证一下:

//// 1
//- (NSString *)getName {
//    NSLog(@"%s",__func__);
//    return NSStringFromSelector(_cmd);
//}
//
//// 2
//- (NSString *)name {
//    NSLog(@"%s",__func__);
//    return NSStringFromSelector(_cmd);
//}
//
//// 3
//- (NSString *)isName {
//    NSLog(@"%s",__func__);
//    return NSStringFromSelector(_cmd);
//}
//
////4
//- (NSString *)_name {
//    NSLog(@"%s",__func__);
//    return NSStringFromSelector(_cmd);
//}

//开启间接访问
+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

@interface Person : NSObject {
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}

//给4个成员变量赋值
    Person *p = [[Person alloc] init];
    p->_name = @"_name";
    p->_isName = @"_isName";
    p->name = @"name";
    p->isName = @"isName";
    
    NSLog(@"name = %@",[p valueForKey:@"name"]);

步骤2[p valueForKey:@"name"]取值结果.png

首先在Person类里面定义4个对应的成员变量并开启间接访问,通过打印结果一样我们可以看到一个奇怪的现象,我们明明访问的是name,但是打印出来的确是@“_name”,接着我们把NSString *_name;注释,第二次再打印就是@“_isName”,后面依次也是一样,这里就印证了步骤2的取值顺序。

  • 步骤3
+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

关闭间接访问的打印.png

当我们关闭间接访问的话,则会报setValue:forUndefinedKey:错误,并抛出异常。

KVC赋值过程

KVC赋值过程.png

这里也是一样,我们只分析普通数据类型的赋值过程,通过文档我们可以看到,赋值跟取值也是类似,分为3个步骤。

  • 步骤1 当我们通过[p setValue:@"123" forKey:@"name"]方法赋值的时候,首先就会在对象内部依次查找set<key>_set<key>方法有没有实现。

  • 步骤2 如果set<key>_set<key>这两个方法没有实现,就会按顺序查找_<key>, _is<Key>, <key>, is<Key>这些成员变量去赋值。

  • 步骤3 当我们关闭间接访问的话,则会报setValue:forUndefinedKey:错误,并抛出异常。

自定义KVC

通过了解KVC的取值赋值过程,我们可以猜测系统的内部实现过程,自己尝试通过代码实现一下。这里是自己demo地址。当然比较简单呀。不管是KVC还是KVO还是建议用系统的,系统内部处理的逻辑肯定要复杂的多,我们这都只是猜测,用的话可能会遇到一些未知的问题。

#import "NSObject+LCX_KVC.h"
#import <objc/runtime.h>

@implementation NSObject (LCX_KVC)

//通过key获取值
- (nullable id)lcx_valueForKey:(NSString *)key {
    // 1.判断key不为空
    if (key == nil  || key.length == 0) {
        return nil;
    }
 
    // 2.找到相关方法 get<Key>, <key>, is<Key>, or _<key>
    // key 要大写
    NSString *Key = key.capitalizedString;
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    NSString *_key = [NSString stringWithFormat:@"_%@",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(isKey)]){
        return [self performSelector:NSSelectorFromString(isKey)];
    } else if ([self respondsToSelector:NSSelectorFromString(_key)]){
        return [self performSelector:NSSelectorFromString(_key)];
    }
#pragma clang diagnostic pop
     
    // 3.判断是否开启间接访问
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"LCXUnknownKeyException" reason:[NSString stringWithFormat:@"[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name",self] userInfo:nil];
    }
    
     // 4.按照 _<key>, _is<Key>, <key>, or is<Key> 顺序查询实例变量
    NSMutableArray *mArray = [self getIvarListName];
    _key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    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);;
    }
    
    // 5.抛出异常
    @throw [NSException exceptionWithName:@"LCXUnknownKeyException" reason:[NSString stringWithFormat:@"[%@ %@]: valueForUndefinedKey:%@",self,NSStringFromSelector(_cmd),key] userInfo:nil];
 
    return @"";
}

//通过可以设置值
- (void)lcx_setValue:(nullable id)value forKey:(NSString *)key {
    // 1.判断key不为空
    if (!key || key.length < 1) {
        return;
    }
    
    // 2.找到相关方法 set<Key> _set<Key>
    // key 首字符要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    
    if ([self performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"-------------------%@",setKey);
        return;
    } else if ([self performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"-------------------%@",_setKey);
        return;
    }
    
    // 3.判断是否开启间接访问
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"LCXUnknowKeyException" 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>, or is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",key.capitalizedString];
    NSString *isKey = [NSString stringWithFormat:@"is%@",key.capitalizedString];
    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;
    } 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:@"LCXUnknowKeyException" reason:[NSString stringWithFormat:@"[%@ %@]: this class is not key value coding-compliant for the key name",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

#pragma mark - Parvite Methods

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

- (NSMutableArray *)getIvarListName {
    
    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);
    return mArray;
}

@end

KVC使用注意事项

  1. value不是NSNumber或者NSValue类型,例如int类型需要转成NSNumber类型。
  2. 赋值时value不能为空。
  3. 赋值的key不纯在。
  4. 取值的key不纯在。

针对以上错误,建议添加一个分类进行错误收集跟防止crash,虽然这些情况出现的概率比较小,但是还是要尽可能的保证项目的稳定性跟健壮性,让用户在使用中经常遇到闪退的话还是比较影响用户体验的。

持续更新改正中......