iOS KVC应用及原理

696 阅读8分钟

前言

  KVCKey-value coding)意思就是键值编码,其允许开发者通过Key名直接访问对象的属性(不管这个属性是不是私有的),或者给对象的属性赋值,而不需要调用明确的存取方法,这样就可以在运行时动态地访问和修改对象的属性,而不是在编译时确定,但是在iOS开发中,如果想要使用KVC,需要类遵守NSKeyValueCoding协议或者此类(直接或者间接)继承自NSObject类,因为苹果对NSObject类提供了NSKeyValueCoding默认实现。

学习重点

KVC的常规使用

KVC原理

1. KVO基本使用

  对KVC的使用不是很了解的可以下载此KVC基本使用 项目工程进行运行查阅, 密码:2swz

2. KVC原理

  对于KVC原理的介绍可以查阅KVC官方文档

2.1 KVC的设值流程

  KVC文档中关于KVC数据属性取值流程主要有3个步骤,如下图所示:

image.png

  翻译过来大致如下:

  setValue:forKey:方法的默认实现:将给定keyvalue参数作为输入,尝试在接收调用的对象内部设置名为key的属性的值为value(或者,对于非对象属性,value的未包装版本,如表示非对象值所述),使用如下过程:

  1. 查找第一个名为set<Key>:_set<Key>setIs<Key>(这是额外补充的)的访问器,按此顺序。如果找到,则使用输入值(或根据需要取消包装的值)调用它并完成。

  2. 如果没有找到简单的访问器,并且如果类方法accessinstancevariablesdirect返回YES,请查找一个实例变量,其名称依次为_<key>_is< key> <key>is< key>。如果找到,直接用输入值(或取消包装的值)设置变量并完成。

  3. 在没有找到访问器或实例变量时,调用setValue:forUndefinedKey:。默认情况下,这会引发一个异常,但是NSObject的子类可能提供特定于键的行为。

  以下为验证过程,首先编写如下代码:

TestObject.h文件代码
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TestObject : NSObject

- (void)printInstanceVariablesValue;

@end

NS_ASSUME_NONNULL_END

TestObject.m文件代码
#import "TestObject.h"

@implementation TestObject {
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
}

- (void)printInstanceVariablesValue {
    NSLog(@"_name = %@ -- _isName = %@ -- name = %@ -- isName = %@", self->_name, self->_isName, self->name, self->isName);
}

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    
    NSLog(@"%s, key = %@, value = %@", __func__, key, value);
}

- (void)setName:(NSString *)name {

    NSLog(@"%s, %@", __func__, name);
}

- (void)_setName:(NSString *)name {

    NSLog(@"%s, %@", __func__, name);
}

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

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

@end

//ViewController.m文件中部分代码
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self setValuefundamentalTest];

}

- (void)setValuefundamentalTest {
    TestObject *testObj = [[TestObject alloc] init];
    
    [testObj setValue:@"哈哈哈" forKey:@"name"];
    
    [testObj printInstanceVariablesValue];
}

  运行程序,控制台输出打印结果如下图所示:

image.png

  然后将TestObject.m文件代码中setName方法注释掉,运行程序,控制台输出打印结果如下图所示:

image.png

  然后将TestObject.m文件代码中_setName方法注释掉,运行程序,控制台输出打印结果如下图所示:

image.png

  然后将TestObject.m文件代码中setIsName方法注释掉,运行程序,控制台输出打印结果如下图所示:

image.png

  可以发现的是并不会接着调用_setIsName方法,而是调用了setValue:forUndefinedKey:方法,并且方法优先调用顺序为:set<Key>:_set<Key>set<IsKey>

  然后将以上的set方法全部注释掉,accessInstanceVariablesDirectly方法的返回值改为YES,执行代码,控制台输出信息如下所示:

image.png

  然后将TestObject.m中的实例变量_name注释掉,执行代码,控制台输出信息如下所示:

image.png

  然后将TestObject.m中的实例变量_isName注释掉,执行代码,控制台输出信息如下所示:

image.png

  然后将TestObject.m中的实例变量name注释掉,执行代码,控制台输出信息如下所示:

image.png

  最后将TestObject.m中的实例变量isName注释掉,执行代码,控制台输出信息如下所示:

image.png

  可以发现对象的实例变量设值优先顺序为: _name_isNamenameisName,如果没有找到以上四个实例变量,就会调用setValue:forUndefinedKey:方法,如果setValue:forUndefinedKey:方法都没有实现,就会报错打印出异常信息,如下所示:

image.png

2.2 KVC的取值流程

  KVC文档中关于KVC的取值流程主要有6个步骤,如下图所示:

image.png

  valueForKey:方法的默认实现,给定一key参数作为输入,执行以下步骤,从类的实例对象接收到valueForKey消息开始进行内部操作。

  1. 在实例对象中搜索一个名称为get<Key><key>is<Key>或者_<key>的访问器方法,如果找到了,就调用它,并获取方法的返回结果执行步骤5,否则执行下一步。

  2. 如果没有找到简单的访问器方法,在实例对象中搜索匹配countOf<Key>objectIn<Key>AtIndex:(对应于NSArray类定义的原始方法)和<Key>atIndexes:(对应于NSArray方法obejctsAtIndexes:)的方法,如果找到了第一个和另外两个中的至少一个,创建一个集合代理对象,响应所有NSArray方法并返回它,否则,执行步骤3。代理对象随后将它就收到的所有NSArray消息转换为countOf<Key>objectIn<Key>AtIndex:<Key>AtIndexes:消息的组合,并将其转换为符合键值编码的创建它的对象。如果原始对象还实现了一个名为get<Key>:range:的可选方法,那么代理对象也会在适当地的时候使用该方法。实际上,代理对象与符合键值编码的对象一起工作,允许底层属性像NSArray一样的行为,即使它不是NSArray

  3. 如果没有找到简单的访问方法或数组访问方法组,则查找名为countOf<Key>enumeratorOf<Key>memberOf<Key>:(对应于NSSet类定义的基本方法)的三个方法。如果找到了上面所有三个方法,创建一个集合代理对象响应所有的NSSet方法并返回它。否则,执行步骤4。这个代理对象随后将它接收到的任何NSSet消息转换为countOf<Key>enumeratorOf<Key>memberOf<Key>:``messages的组合,并将其转换为创建它的对象。实际上,代理对象与符合键值编码的对象一起工作,允许底层属性的行为就像它是一个NSSet,即使它不是。

  4. 如果没有找到简单的访问方法或一组集合访问方法,并且如果接收方的类方法accessinstancevariablesdirect返回YES,则依次搜索实例变量_<key>_is< key><key>is<key>,如果找到,直接获取实例变量的值,然后继续步骤5。否则,执行步骤6

  5. 如果检索到的属性值是对象指针,只需返回结果。如果该值是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回该实例。如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回该对象。

  6. 如果其他方法都失败了,调用valueForUndefinedKey:,默认情况下,这会引发一个异常,但是NSObject的子类可能提供特定于键的行为。

  探究验证流程如下所示(本文仅仅探究非集合类型的实例变量):

  编写如下代码:

//TestObject.h文件代码
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TestObject : NSObject

- (void)setInstanceVariablesValue;

@end

NS_ASSUME_NONNULL_END

//TestObject.m文件代码
#import "TestObject.h"

@implementation TestObject {
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
}

- (void)setInstanceVariablesValue {
    self->name    = @"name";
    self->_name   = @"_name";
    self->_isName = @"_isName";
    self->isName  = @"isName";
}

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

- (NSString *)getName {
    NSLog(@"%s", __func__);

    return @"呼呼";
}

- (NSString *)name {
    NSLog(@"%s", __func__);

    return @"嘻嘻";
}

- (NSString *)getIsName {
    NSLog(@"%s", __func__);

    return @"呼呼";
}

- (NSString *)isName {
    NSLog(@"%s", __func__);

    return @"吱吱";
}

- (NSString *)get_Name {
    NSLog(@"%s", __func__);

    return @"呼呼";
}

- (NSString *)_name {
    NSLog(@"%s", __func__);

    return @"吱吱";
}

- (NSString *)get_isName {
    NSLog(@"%s", __func__);

    return @"呼呼";
}

- (NSString *)_isName {
    NSLog(@"%s", __func__);

    return @"吱吱";
}

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"key = %@", key);
    return @"";
}

@end

//ViewController.m文件中部分代码
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self getValuefundamentalTest];
}

- (void)getValuefundamentalTest {
    TestObject *testObj = [[TestObject alloc] init];
    
    [testObj setInstanceVariablesValue];

    NSString *name = [testObj valueForKey:@"name"];
    
    NSLog(@"%@", name);
}

  运行程序,控制台打印信息如下所示:

image.png

  将TestObject.m文件中的getName方法注释掉,运行程序,控制台打印信息如下所示:

image.png

  将TestObject.m文件中的name方法注释掉,运行程序,控制台打印信息如下所示:

image.png

  将TestObject.m文件中的isName方法注释掉,运行程序,控制台打印信息如下所示:

image.png

  将TestObject.m文件中的_name方法注释掉,运行程序,控制台打印信息如下所示:

image.png

  可以发现,实例变量nameget方法优先调用顺序为:getNamenameisName_name,接着将以上的方法全部注释,再将accessInstanceVariablesDirectly函数的返回值设置为YES,运行程序,控制台打印信息如下所示:

image.png

  将实例变量_name注释掉,运行程序,控制台打印信息如下所示: image.png

  将实例变量_isName注释掉,运行程序,控制台打印信息如下所示:

image.png

  将实例变量name注释掉,运行程序,控制台打印信息如下所示:

image.png

  将实例变量isName注释掉,运行程序,控制台打印信息如下所示:

image.png

  如果再将valueForUndefinedKey方法注释掉,程序就会报错崩溃,如下图所示:

image.png

  可以发现的是,对象中的实例变量的获取优先顺序为:_name_isNamenameisName