iOS底层原理-KVC

324 阅读8分钟

前言

在iOS开发中,经常会被问到关于KVC的概念,那什么是KVC呢?KVC到底可以做什么?这篇就对KVC做个简单的分析,KVC全称是Key-Value Coding,也就是键值编码,我们可以从苹果的官方文档(Key-Value Coding Programming Guide)中看到这样一段描述。

键值编码是一种由NSKeyValueCoding非正式协议启用的机制,对象采用它来提供对其属性的间接访问。当一个对象符合键值编码时,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数来寻址。这种间接访问机制补充了实例变量及其关联的访问方法所提供的直接访问。 我们可以通过setValue:forKey:给属性设值,通过valueForKey:获取属性值。

KVC兼容对象

当对象从NSObject继承时(直接或间接地),它们通常采用键值编码,两者都采用NSKeyValueCoding协议,并为基本方法提供默认实现。这样的对象允许其他对象通过消息传递接口完成以下工作:

  • 访问对象的属性
  • 操作集合属性
  • 在集合对象上调用集合操作符
  • 访问非对象属性
  • 通过keyPath访问属性

KVC设值和取值

那KVC是如何给对象属性进行设值和取值的?我们根据官方文档说明来研究一下这个流程。

setter

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

  1. 查找第一个名为set<Key>:_set<Key>的访问器,按此顺序。如果找到,则使用输入值(或根据需要取消包装的值)调用它并完成。
  2. 如果没有找到简单的访问器,并且如果类方法accessinstancevariablesdirect返回YES,请查找一个实例变量,其名称依次为_<key>_is<Key><key>is<Key>。如果找到,直接用输入值(或取消包装的值)设置变量并完成。
  3. 在没有找到访问器或实例变量时,调用setValue:forUndefinedKey:。默认情况下,这会引发一个异常,但是NSObject的子类可能提供指定Key的行为。 下面通过一个简单的示例进行验证,实际代码调试来玩一玩KVC的setter过程。
setter示例代码

在工程项目中创建一个类ATPerson,增加2个方法实现setName:_setName:,并在实现里增加打印信息,在ViewController中调用setValue:forKey:看看打印结果,代码如下:

ViewController.m

#import "ViewController.h"
#import "ATPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    ATPerson *p = [[ATPerson alloc] init];
    [p setValue:@"Atom" forKey:@"name"];
}
@end

ATPerson.m

#import "ATPerson.h"

@implementation ATPerson

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

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

@end

运行代码,结果输出-[ATPerson setName:] -- Atom,执行了setName:方法。 001.png 下面我们把setName:注释掉,再次运行,可以看到执行了_setName:方法。 002.png 通过上面的代码结果可以验证了setter的第1个说明,接下来我们对第2条进行验证。在ATPerson.h增加对应的4个实例变量,然后在.m文件中增加accessinstancevariablesdirect函数并返回YES

ATPerson.h

#import <Foundation/Foundation.h>

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

@end

ATPerson.m

#import "ATPerson.h"

@implementation ATPerson

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

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

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

@end

ViewController.m中增加打印信息

- (void)viewDidLoad {
    [super viewDidLoad];
    
    ATPerson *p = [[ATPerson alloc] init];
    [p setValue:@"Atom" forKey:@"name"];
    
    NSLog(@"取值-- _name = %@, _isName = %@, name = %@, isName = %@", 
            p->_name, p->_isName, p->name, p->isName);
}

再次运行输出,可以看到_name设置了值,其他的都是null003.png 注释掉_name,再次运行,可以看到_isName设置了值。这就验证了第2条的说明。 004.png 根据实例变量的这几种情况,我们猜测setter方法是否也有setIsName_setIsName,同样在ATPerson.m里增加这2个方法。运行 005.png 输出setIsName:,接下来注释掉setIsName:,只留一个_setIsName:,再次运行,发现没有输出,这就得出一个结论,在第1个苹果官方给的2个方法,其实还有一个setIsName:也会执行。对于上面2条都没有实现的话就走到第3个流程,调用setValue:forUndefinedKey:并抛出异常。

以上就是KVC中setter的分析流程。

getter

valueForKey:的默认实现,给定一个key参数作为输入,执行以下过程,从接收valueForKey的类实例执行内部操作:

  1. 在实例中搜索第一个名称为get<Key><key>is<Key>_<key>的访问器方法。如果找到了,就调用它,并使用结果继续步骤5。否则执行下一步
  2. 如果没有找到简单的访问器方法,在实例中搜索匹配模式countOf<Key>objectIn<Key>AtIndex:(对应于NSArray类定义的原始方法)和<key>AtIndexes:(对应于NSArray方法objectsAtIndexes:)的方法。
  • 如果找到了第一个和另外两个中的至少一个,创建一个集合代理对象,响应所有NSArray方法并返回它。否则,执行步骤3。
  • 代理对象随后将它接收到的所有NSArray消息转换为countOf<Key>,objectIn<Key>AtIndex:,和<key>AtIndexes:消息的组合,并将其转换为符合键值编码的创建它的对象。如果原始对象还实现了一个名称为get<Key>:range:的可选方法,那么代理对象也会在适当的时候使用该方法。实际上,代理对象与符合键值编码的对象一起工作,允许底层属性像NSArray一样行为,即使它不是NSArray。
  1. 如果没有找到简单的访问方法或数组访问方法组,则查找名为countOf<Key>,enumeratorOf<Key>,和memberOf<Key>:(对应于NSSet类定义的基本方法)的三个方法。
  • 如果找到了所有三个方法,创建一个集合代理对象响应所有的NSSet方法并返回它。否则,执行步骤4。
  • 这个代理对象随后将它接收到的任何NSSet消息转换为countOf<Key>,enumeratorOf<Key>memberOf<Key>:消息的组合,并将其转换为创建它的对象。实际上,代理对象与符合键值编码的对象一起工作,允许底层属性的行为就像它是一个NSSet,即使它不是。
  1. 如果没有找到简单的访问方法或一组集合访问方法,并且如果接收方的类方法accessinstancevariablesdirect返回YES,则搜索实例变量_<key>,_is<Key>,<key>is<Key>,如果找到,直接获取实例变量的值,然后继续步骤5。否则,执行步骤6。
  2. 如果检索到的属性值是对象指针,只需返回结果。如果该值是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回该实例。如果结果是NSNumber不支持的标量类型,则转换为NSValue对象并返回该对象。
  3. 如果其他方法都失败了,调用valueForUndefinedKey:。默认情况下,这会引发一个异常,但是NSObject的子类可能提供特定于键的行为。
getter示例代码

同样在工程项目中ATPerson.m中增加以下方法实现,对getter进行输出验证。

- (NSString *)getName {
    return NSStringFromSelector(_cmd);
}

- (NSString *)name {
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName {
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name {
    return NSStringFromSelector(_cmd);
}

然后在ViewController.m中通实例变量去赋值,KVC取值的方式进行打印输出。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    ATPerson *p = [[ATPerson alloc] init];
    // KVC 设值过程
    // [p setValue:@"Atom" forKey:@"name"];
    // NSLog(@"取值-- _name = %@, _isName = %@, name = %@, isName = %@", 
                p->_name, p->_isName, p->name, p->isName);
    
    // KVC 取值过程
    p->_name = @"_name";
    p->_isName = @"_isName";
    p->name = @"name";
    p->isName = @"isName";
    NSLog(@"取值 -- %@", [p valueForKey:@"name"]);
}

运行代码,可以看到输出getName006.pngATPerson.mgetName方法注释掉再次运行,输出name007.png 以此类推,接下来就会走isName_name的方法调用。这就验证了第1个步骤,KVC的getter会按照get<Key><key>is<Key>_<key>的顺序依次查找对应的实现方法,有就返回。如果在步骤1种没有实现,就会按着下面几个步骤依次执行,最终如果还是没有就走到valueForUndefinedKey:并抛出异常,后面的步骤由于篇幅问题就不做一一验证了。

KVC的自定义

根据KVC的官方文档说明,我们依次验证了settergetter的的流程,了解了它的原理,那我们能不能自己去实现一个类似于KVC的类呢。

设计思路

  1. 判断key值是否为空
  2. 判断是否有set<Key>或者_set<Key>方法
  3. 获取相关实例变量
    • 判断accessInstanceVariablesDirectly是否返回YES
    • 判断是否有_<key>_is<Key><key>is<Key>相关的实例变量,并对实例变量进行赋值
  4. 如果都没有,报出异常 以上是setter方法的设计思路流程,效仿苹果官方的步骤,getter也是类似,代码偏多就不贴到这里了,有兴趣的同学可以移步gitee仓库地址

总结

以上就是对iOS中KVC的原理分析,虽然苹果对于KVC没有提供源码,但可以借助官方文档说明去分析,通过代示例去验证,对于开发中,只有不断的探索分析才能了解底层的设计思路,去理解并运用在我们的实际项目开发中。