iOS中的KVC简介

简介

Key-Value Coding 俗称"键值编码",苹果官方简称这个模式为KVC编码模式,也就是说可以通过一个Key去访问某一个属性,或者给对象去赋值,而不需要去明确存取方法,这样就可以动态的访问和修改对象的属性,而不是在编译的时候去确定,这也是iOS开发中的一大便利,其实有很多的框架和功能是用KVC去实现的,这个技术存在已经很长时间了,在网上也有很多相关的教程去教童鞋们如何去使用KVC,在这里,我们就只是简单的介绍一下KVC的底层实现和使用方法。

KVC在 Apple Document中的解释:

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.

You typically use accessor methods to gain access to an object’s properties. A get accessor (or getter) returns the value of a property. A set accessor (or setter) sets the value of a property. In Objective-C, you can also directly access a property’s underlying instance variable. Accessing an object property in any of these ways is straightforward, but requires calling on a property-specific method or variable name. As the list of properties grows or changes, so also must the code which accesses these properties. In contrast, a key-value coding compliant object provides a simple messaging interface that is consistent across all of its properties.

Key-value coding is a fundamental concept that underlies many other Cocoa technologies, such as key-value observing, Cocoa bindings, Core Data, and AppleScript-ability. Key-value coding can also help to simplify your code in some cases.

从苹果官方对KVC的解释来看,其实KVC在Fundation框架中占有很高的地位,诸如Core-Data之类的框架都使用到了KVC技术,我们在开发中可能常见的API有:

- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key; 

NSKeyValueCoding类别中还有其他的一些比较重要方法,如下:

// 默认返回YES,表示如果没有找到set(get)<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索。
+ (BOOL)accessInstanceVariablesDirectly;
// 如果Key不存在,且无法搜索到任何和Key有关的字段或者属性,则最后才会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
// 和方法valueForUndefinedKey:一样,但这个方法是设值的时候才调用
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 如果调用setValue方法时给Value传nil,则会调用这个方法。
- (void)setNilValueForKey:(NSString *)key;
// KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 集合操作的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回,这个用于KVC监听属性为NSMutableArray*类型。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

2.KVC执行流程

赋值

说起KVC的执行流程,我们有很多初级工程师都不大清楚,只知道KVC是如何使用的,而不知道KVC是怎么Key的寻找策略的。下图我们借鉴了MJ老师的两幅PPT来解释

SetValueForKey.jpg

上图我们可以看到

  1. 程序优先调用set<Key>:_set<Key>方法,代码通过setter方法完成设置。注意,这里的<key>是指成员变量名,首字母大小写要符合KVC的命名规则,下同

  2. 如果没有找到set<Key>:方法,KVC机制会搜索该类里面有没有名为<key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以<key>命名的变量,KVC都可以对该成员变量赋值。

  3. 如果该类即没有set<Key>:方法,也没有_<key>成员变量,KVC机制会搜索_is<Key>的成员变量。

  4. 和上面一样,如果该类即没有set<Key>:方法,也没有_<key>_is<Key>成员变量,KVC机制再会继续搜索<key>is<Key>的成员变量。再给它们赋值。

  5. 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。

简单说KVC机制在设值的时候会按照set<Key>: 》_set<Key> 》_<key> 》_is<Key> 》<key> 》 is<Key>顺序搜索成员并进行赋值操作,但是如果开发者重写了类方法+ (BOOL)accessInstanceVarialbesDirectly并且让其返回NO,这样在搜索的时候会直接从步骤1跳转到步骤5

举一个例子,我们先创建一个Person类

@interface Person()
{
    NSString* _name;
    NSString* _isName2;
    NSString* _isName3;
    NSString* name3;
    NSString* name4;
    NSString* isName4;
    NSString* isName5;
}

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

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    NSLog(@"%s value=%@, 该key=%@不存在!", __FUNCTION__, value, key);
}

- (id)valueForUndefinedKey:(NSString *)key
{
    NSLog(@"%s,该key不存在%@", __FUNCTION__, key);
    return nil;
}

然后用KVC赋值

    _p = [[Person alloc]init];
    [_p setValue:@"Leon1" forKey:@"name"];
    [_p setValue:@"Leon2" forKey:@"name2"];
    [_p setValue:@"Leon3" forKey:@"name3"];
    [_p setValue:@"Leon4" forKey:@"name4"];
    [_p setValue:@"Leon5" forKey:@"name5"];
    [_p setValue:@"Leon6" forKey:@"name6"];

最终在控制台打印的结果是

function:-[Person setName:] line:50 content:-[Person setName:] name=Leon1
function:-[Person setValue:forUndefinedKey:] line:56 content:-[Person setValue:forUndefinedKey:] value=Leon6, 该key=name6不存在!

取值

当调用valueForKey:方法时,KVC对key的搜索顺序有点不同于setValue:forKey:方法,大致步骤如下:

ValueForKey.jpg
  1. 按顺序查找方法get<Key>, <key>, is<Key>,如果其中一种方法找到直接调用,如果是BOOL或者int等值类型,会将包装成NSNumber对象。

  2. 如果步骤1的几个getter方法都没有找到,KVC机制会查找是否实现了方法countOf<Key>,同时还实现了两个方法(objectIn<Key>AtIndex<Key>AtIndexes)中的一个即可。如果都实现就会返回一个可以响应NSArray所有方法的代理集合(它是 NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes这几个方法组合的形式调用,在这几个函数打断点查看会发现还有一个可选方法get<Key>:range:。所以想重新定义KVC的一些功能,可以添加这些方法,添加的时候注意方法名要符合KVC标准命名方法。

  3. 步骤2没有找到,就会同时查找countOf<Key>enumeratorOf<Key>memberOf<Key> 格式的方法。如果这三个方法都找到,就会返回一个可以响应NSSet所有方法的代理集合,同步骤2一样的形式调用。

  4. 步骤3也没找到就会检查类方法+ (BOOL)accessInstanceVarialbesDirectly,如果返回是YES的,和设值一样的顺序,按_<key>_is<Key><key>is<Key>搜索成员变量名,当然这种方法不推荐这么做,这样直接访问实例变量破坏了封装性,使代码更脆弱。这样还没找到就会调用valueForUndefinedKey:方法,默认抛出异常。

    NSString*name1 =  [_p valueForKey:@"name"];
    NSString*name2 = [_p valueForKey:@"name2"];
    NSString*name3 = [_p valueForKey:@"name3"];
    NSString*name4 = [_p valueForKey:@"name4"];
    NSString*name5 = [_p valueForKey:@"name5"];
    NSString*name6 = [_p valueForKey:@"name6"];
    
    NSLog(@"\nname1 = %@\n, name2 = %@\n, name3= %@\n, name4 = %@\n, name5 = %@\n , name6 = %@\n",name1,name2,name3,name4,name5,name6);

最终打印为

function:-[Person valueForUndefinedKey:] line:61 content:-[Person valueForUndefinedKey:],该key不存在name6
function:-[TransDict setKVC] line:94 content:
name1 = Leon1
, name2 = Leon2
, name3= Leon3
, name4 = Leon4
, name5 = Leon5
, name6 = (null)

上述可以看出,当Key查找不到值的时候会走valueForUndefinedKey方法中抛出异常

- (id)valueForUndefinedKey:(NSString *)key
{
    NSLog(@"%s,该key不存在%@", __FUNCTION__, key);
    return nil;
}

KVC使用keyPath

类的成员变量有可能是自定义类或其他复杂数据类型,对这种成员变量可以先用KVC获取该属性,然后再用KVC来获取这个自定义类的属性,这样一层层去获取,但这样比较繁琐。对此KVC提供一个解决方案,就是键路径keyPath,顾名思义就是按照路径寻找key。主要有两个以下两个方法:

- (nullable id)valueForKeyPath:(NSString *)keyPath;                
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

在上述Person中我们创建一个Cat类

@property(strong,nonatomic) Cat *cat;

在Cat类中我们创建一个属性name

@interface Cat : NSObject

@property(copy,nonatomic) NSString *name;

@end

我们如果需要用KVC对Person对象中Cat对象赋值的话,我们就必须用到KeyPath了

[_p setValue:@"凯迪" forKeyPath:@"cat.name"];

KVC对于keyPath的搜索机制第一步就是分离key,用小数点.来分割key,然后再像普通key一样按照上面介绍的顺序搜索。

KVC处理异常

使用KVC过程中最常见的异常就是不小心使用了错误的key,或者在设值中不小心传了nil的值,KVC有专门的方法处理这些异常。

  • KVC处理nil异常,如果在设值过程中,不小心传了nil,KVC会调用方法setNilValueForKey:,这个默认方法是抛出异常,所以一般而言最好重写这个方法。

  • KVC处理UndefinedKey异常,如果在设值取值传的key不存在,这样的操作就会crash,设值会调用到setValue:forUndefinedKey:方法,而取值会调用valueForUndefinedKey:方法,这两个方法默认都是抛出异常,所以最好重写这两个方法来规避crash。

KVC处理容器类属性

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

该方法返回一个可变有序数组。对于无序的容器,可以用以下方法:

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

该方法返回一个可变的无序集合。同时他们也有对应的keyPath版本:

- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

当NSDictionary对象使用KVC时,valueForKey:的表现行为和objectForKey:一样,使用valueForKeyPath:可访问多层嵌套的字典会方便点,在KVC中有两个关于NSDictionary的方法:

// 输入一组key,返回这组key对应的属性,再组成一个字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
// 用来修改Model中对应key的属性
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

KVC的验证

当开发者需要验证能不能用KVC设定某个值时,就需要在进行KVC赋值前验证值value的有效性,API文档里面提供下面的方法进行判断值的有效性。

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

该方法的工作原理:先找一下你的类中是否实现了方法-(BOOL)validate<Key>:error:,如果实现了就会根据实现方法里面的自定义逻辑返回NO或者YES,如果没有实现这个方法,则系统默认返回就是YES。

#import "JJKVCVerifiedVC.h"

@interface JJKVCVerifiedVC ()

@property (nonatomic, copy) NSString *personName;

@end

@implementation JJKVCVerifiedVC

#pragma mark - Override Base Function

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    NSError *error;
    NSString *key = @"personName";
    NSString *value = @"小明";
    BOOL result = [self validateValue:&value forKey:key error:&error];
    
    if (error) {
        NSLog(@"error = %@", error);
        return;
    }
    
    if (result) {
        NSLog(@"验证正确是小明");
    }
    else {
        NSLog(@"不是小明");
    }
}

#pragma mark - Object Private Function

- (BOOL)validatePersonName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
{
    NSString *name = *value;
    if ([name isEqualToString:@"小明"]) {
        return YES;
    }
    return NO;
}

@end

输出结果为

2017-09-12 22:09:42.878 JJOC[9834:292358] 验证正确是小明

这里首先调用方法[self validateValue:&value forKey:key error:&error];,这里,由于我实现了方法- (BOOL)validatePersonName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError,所以就在这里进行值value有效性的判断,这里[name isEqualToString:@"小明"]我就给返回YES,否则就返回NO。

写在后面

KVC在iOS开发中非常的灵活,提供了开发者更多的赋值和取值操作的选择,它的有点明显,缺点也有,如果key只写错,编写的时候不会报错,但是运行的时候会报错,在实际开发中需要开发者时刻小心自己输入的键值,也时刻提醒着开发者一旦使用KVC就要做容错处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,175评论 1 32
  • 本文参考: KVC官方文档 KVC原理剖析 iOS KVC详解 KVC 简介 KVC全称是Key Value Co...
    拧发条鸟xds阅读 5,363评论 6 23
  • 原文:iOS 关于KVC的一些总结 本文参考: KVC官方文档 KVC原理剖析 iOS KVC详解 KVC 简介 ...
    liyoucheng2014阅读 971评论 0 3
  • KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iO...
    我的梦工厂阅读 907评论 1 8
  • KVC(Key-valuecoding)键值编码,单看这个名字可能不太好理解。其实翻译一下就很简单了,就是指iOS...
    榕樹頭阅读 740评论 0 2