KVC底层原理

318 阅读9分钟

什么是KVC

KVC(Key-value coding)键值编码,苹果官方文档中是这样介绍的:它是一种通过字符串描述符而不是通过调用访问方法或者直接使用实例变量的非直接的访问对象属性的机制。就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。

无论是Swift还是Objective-C,KVC的定义都是对NSObject的扩展来实现的(Objective-C中有个显式的NSKeyValueCoding类别名,而Swift没有,也不需要)。所以对于所有继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC(一些纯Swift类和结构体是不支持KVC的),下面是KVC最为重要的四个方法

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

NSKeyValueCoding类别中还有其他的一些方法,下面列举一些

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

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

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。

- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。

- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。

同时苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。

有序集合对应方法如下:

-countOf<Key>//必须实现,对应于NSArray的基本方法count:2  -objectIn<Key>AtIndex:

-<key>AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://可选的,如果在此类操作上有性能问题,就需要考虑实现之

无序集合对应方法如下:

-countOf<Key>//必须实现,对应于NSArray的基本方法count:

-objectIn<Key>AtIndex:

-<key>AtIndexes://这两个必须实现一个,对应于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:

-get<Key>:range://不是必须实现的,但实现后可以提高性能,其对应于 NSArray 方法 getObjects:range:

-insertObject:in<Key>AtIndex:

-insert<Key>:atIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:

-removeObjectFrom<Key>AtIndex:

-remove<Key>AtIndexes://两个必须实现一个,类似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:

-replaceObjectIn<Key>AtIndex:withObject:

-replace<Key>AtIndexes:with<Key>://这两个都是可选的,如果在此类操作上有性能问题,就需要考虑实现之

KVC取值流程

valueForKey:执行过程

  • 在实例对象中按照get<Key>, <key>, is<Key>, _<key>,这个顺序来查找方法,如果找到了就调用它并继续执行第五步,否则进行下一步
  • 没有找到第一步的getter方法,则匹配以下方法 countOf<Key>,objectIn<Key>AtIndex:,<key>AtIndexes:。如果第一个方法和至少另外两个方法中的一个被找到,则创建一个集合代理对象来响应所有的NSArray方法并返回它。否则,继续下一步。该对象调用以上方法,会调用valueForKey:方法。(NSArray类的方法)
  • 如果没有找到getter方法和数组访问方法,则匹配以下方法countOf<Key>,enumeratorOf<Key>,and memberOf<Key>:。如果这三个方法都找到了,则创建一个集合代理对象来响应所有的NSSet方法并返回它。否则,继续执行步骤4。
  • 如果上述步骤都失败了,且接收方的类方法accessInstanceVariablesDirectly返回YES,按照_<key>, _is<Key>, <key>, is<Key>,这个顺序来匹配实例变量,如果匹配到了,则直接获取实例变量的值并继续步骤5。否则,继续步骤6。
  • 如果匹配到的属性值是对象指针,则只需返回结果。如果值是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回。如果结果是不被NSNumber支持的标量类型,转换成NSValue对象并返回它。
  • 如果所有的步骤都失败了,则调用valueForUndefinedKey:抛出NSUndefinedKeyException异常

例子如下:


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

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

@end

NS_ASSUME_NONNULL_END

#import "Test.h"

@implementation Test

#pragma mark - get相关

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

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

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

- (id)valueForUndefinedKey:(NSString *)key{
    NSLog(@"这个Key不存在:%@",key);
    return nil;
}

#pragma mark - 关闭或开启实例变量赋值
// 是否开启间接访问
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}
@end

调用如下:

#import "ViewController.h"
#import "Test.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    Test *t = [Test new];
    
    [t setValue:@"name" forKey:@"name"];
//    t->_name   = @"_name";
//    t->_isName = @"_isName";
//    t->name    = @"name";
//    t->isName  = @"isName";
    NSLog(@"%@",[t valueForKey:@"name"]);
}
@end

输出如下:

2020-01-10 22:16:10.428438+0800 KVC_Demo[3408:44875] -[Test getName]
2020-01-10 22:16:10.428521+0800 KVC_Demo[3408:44875] getName

把get相关注释后把相关赋值代码注释打开再运行

#pragma mark - get相关

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    Test *t = [Test new];
    
    [t setValue:@"name" forKey:@"name"];
    t->_name   = @"_name";
    t->_isName = @"_isName";
    t->name    = @"name";
    t->isName  = @"isName";
    NSLog(@"%@",[t valueForKey:@"name"]);
}
@end

输出如下:

2020-01-10 22:19:44.365156+0800 KVC_Demo[3520:46905] _name

KVC赋值流程

setValue:forKey:执行过程

  • 首先按照set<Key>:,_set<Key>,_setIs<Key>去匹配方法,如果匹配上了,则传入输入的值并调用它。
  • 如果没匹配上且类方法accessInstanceVariablesDirectly返回YES,就按照_<key>, _is<Key>, <key>, is<Key>这个方法去匹配实例变量。如果匹配到,则使用输入值直接设置变量并完成。
  • 没有匹配到方法和实例变量时,就执行setValue:forUndefinedKey:这个方法,抛出NSUndefinedKeyException异常

例子如下:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

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

@end

NS_ASSUME_NONNULL_END

#import "Test.h"

@implementation Test

#pragma mark - set相关
- (void)setName:(NSString *)name{
    NSLog(@"%s",__func__);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s",__func__);
}

- (void)setIsName:(NSString *)isName{
    NSLog(@"%s",__func__);
}

#pragma mark - 关闭或开启实例变量赋值
// 是否开启间接访问
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"这个Key不存在:%@",key);
}
@end

调用代码:

#import "ViewController.h"
#import "Test.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    Test *t = [Test new];
    
    [t setValue:@"name" forKey:@"name"];
    
    NSLog(@"%@",t->name);
    NSLog(@"%@-%@",t->name,t->isName);
    NSLog(@"%@-%@-%@",t->name,t->isName,t->_isName);
    NSLog(@"%@-%@-%@-%@",t->name,t->isName,t->_isName,t->_name);
    
}
@end

输出如下:

2020-01-10 21:58:56.268368+0800 KVC_Demo[2937:37745] -[Test setName:]
2020-01-10 21:58:56.268454+0800 KVC_Demo[2937:37745] (null)
2020-01-10 21:58:56.268520+0800 KVC_Demo[2937:37745] (null)-(null)
2020-01-10 21:58:56.268578+0800 KVC_Demo[2937:37745] (null)-(null)-(null)
2020-01-10 21:58:56.268644+0800 KVC_Demo[2937:37745] (null)-(null)-(null)-(null)

把set相关的代码全部注释之后再运行:

//- (void)setName:(NSString *)name{
//    NSLog(@"%s",__func__);
//}
//
//- (void)_setName:(NSString *)name{
//    NSLog(@"%s",__func__);
//}
//
//- (void)setIsName:(NSString *)isName{
//    NSLog(@"%s",__func__);
//}

输出如下:

2020-01-10 22:00:34.788948+0800 KVC_Demo[2992:38865] (null)
2020-01-10 22:00:34.789027+0800 KVC_Demo[2992:38865] (null)-(null)
2020-01-10 22:00:34.789090+0800 KVC_Demo[2992:38865] (null)-(null)-(null)
2020-01-10 22:00:34.789154+0800 KVC_Demo[2992:38865] (null)-(null)-(null)-name

KVC进阶用法

KVC字典转模型

NSDictionary* dict = @{
                       @"name":@"name",
                       @"nick":@"nick",
                       @"subject":@"subject",
                       @"age":@18,
                       };
Person *p = [[Person alloc] init];
// 字典转模型
[p setValuesForKeysWithDictionary:dict];
NSLog(@"%@",p);
// 键数组转模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
NSLog(@"%@",dic);

KVC消息传递

NSArray *array = @[@"Arg1",@"Arg22",@"Arg333",@"Arg4444"];
NSArray *lenStr= [array valueForKeyPath:@"length"];
NSLog(@"%@",lenStr);// 消息从array传递给了string
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"%@",lowStr);

KVC聚合操作符

NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
    LGPerson *p = [LGPerson new];
    NSDictionary* dict = @{
                           @"name":@"Tom",
                           @"age":@(18+i),
                           @"nick":@"Cat",
                           @"length":@(180 + 2*arc4random_uniform(6)),
                           };
    [p setValuesForKeysWithDictionary:dict];
    [personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"]);

float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
NSLog(@"%f", avg);

int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
NSLog(@"%d", count);

int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
NSLog(@"%d", sum);

int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
NSLog(@"%d", max);

int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
NSLog(@"%d", min);

KVC数组操作符

NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
    LGPerson *p = [LGPerson new];
    NSDictionary* dict = @{
                           @"name":@"Tom",
                           @"age":@(18+i),
                           @"nick":@"Cat",
                           @"length":@(175 + 2*arc4random_uniform(6)),
                           };
    [p setValuesForKeysWithDictionary:dict];
    [personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"]);
// 返回操作对象指定属性的集合
NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.length"];
NSLog(@"arr1 = %@", arr1);
// 返回操作对象指定属性的集合 -- 去重
NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];
NSLog(@"arr2 = %@", arr2);

KVC嵌套集合(array&set)操作

/// array
NSMutableArray *personArray1 = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
    LGPerson *person = [LGPerson new];
    NSDictionary* dict = @{
                           @"name":@"Tom",
                           @"age":@(18+i),
                           @"nick":@"Cat",
                           @"length":@(175 + 2*arc4random_uniform(6)),
                           };
    [person setValuesForKeysWithDictionary:dict];
    [personArray1 addObject:person];
}

NSMutableArray *personArray2 = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
    LGPerson *person = [LGPerson new];
    NSDictionary* dict = @{
                           @"name":@"Tom",
                           @"age":@(18+i),
                           @"nick":@"Cat",
                           @"length":@(175 + 2*arc4random_uniform(6)),
                           };
    [person setValuesForKeysWithDictionary:dict];
    [personArray2 addObject:person];
}

// 嵌套数组
NSArray* nestArr = @[personArray1, personArray2];

NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
NSLog(@"arr = %@", arr);

NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
NSLog(@"arr1 = %@", arr1);

/// set
NSMutableSet *personSet1 = [NSMutableSet set];
for (int i = 0; i < 6; i++) {
    LGPerson *person = [LGPerson new];
    NSDictionary* dict = @{
                           @"name":@"Tom",
                           @"age":@(18+i),
                           @"nick":@"Cat",
                           @"length":@(175 + 2*arc4random_uniform(6)),
                           };
    [person setValuesForKeysWithDictionary:dict];
    [personSet1 addObject:person];
}
NSLog(@"personSet1 = %@", [personSet1 valueForKey:@"length"]);

NSMutableSet *personSet2 = [NSMutableSet set];
for (int i = 0; i < 6; i++) {
    LGPerson *person = [LGPerson new];
    NSDictionary* dict = @{
                           @"name":@"Tom",
                           @"age":@(18+i),
                           @"nick":@"Cat",
                           @"length":@(175 + 2*arc4random_uniform(6)),
                           };
    [person setValuesForKeysWithDictionary:dict];
    [personSet2 addObject:person];
}
NSLog(@"personSet2 = %@", [personSet2 valueForKey:@"length"]);

// 嵌套set
NSSet* nestSet = [NSSet setWithObjects:personSet1, personSet2, nil];

// 交集
NSArray* arr1 = [nestSet valueForKeyPath:@"@distinctUnionOfSets.length"];
NSLog(@"arr1 = %@", arr1);


参考文章
iOS KVC和KVO详解
苹果官方文档 About Key-Value Coding