本文为L_Ares个人写作,以任何形式转载请表明原文出处。
资料准备 : AppleDevelopment - KVC文档
代码准备 : 创建一个
Project
--->App
。创建JDPerson
类。添加NSString
类型的成员变量
,_name
,_isName
,name
,isName
。
上一节主要介绍了一些KVC
的基本信息,包括知道了KVC
本身是一种间接访问机制,提供的是直接访问实例变量的setter
和getter
,还有一些常见或者常用的API
。
本节依然从文档入手,探索KVC
的访问模式是怎样的。
首先从最最常见的setValueForKey
来看,在我们对一个类的某个属性利用KVC
进行赋值的时候,比如说JDPerson
有个name
属性,访问的一般是name
的setter
或者getter
,那么这就会影响对KVC
本身的探索,所以为了纯净环境下探索KVC
的访问模式,我就选择相对更纯净一点的成员变量
方式,而不是通过属性的方式。
一、KVC的访问模式——设值过程
先看一下官方文档里面对于KVC访问模式
中的Setter
的一些知识。
根据自己的理解,加上翻译,可以将官方文档给出的三个知识点总结 :
【前言】 : setValue:forKey:
这个方法的默认实现,给定key
和value
作为输入参数,尝试将名为key
的变量的值设置为value
(对于非对象属性
,也就是上一节中见到过的结构体
,是需要进行一步包装的),设置的流程如下所述 :
- 第一,先寻找
set
或_set
方法,如果找到了就调用set
或者_set
方法进行赋值。
- 第二,如果没有找到
set
或者_set
方法,并且调用KVC
的对象的类的accessInstanceVariablesDirectly
方法返回值是YES
,那么就按照顺序
查找具有如下名称的实例变量(拿name
举例),_name
、_isName
、name
、isName
。如果找到这四个之一,直接对其进行赋值为value
,并且完成赋值。
- 第三,如果第一,第二条全都不满足,那就调用
setValue:forUndefinedKey:
。本来默认是会抛出异常的,但是NSObject
的一个子类提供了这个方法来处理异常的key
。
按照步骤,举个例子 :
(1). 验证上面的第1点 :
JDPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface JDPerosn : NSObject
{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@end
NS_ASSUME_NONNULL_END
JDPerson.m
#import "JDPerosn.h"
@implementation JDPerosn
#pragma mark - 开启或关闭实例变量的赋值
//默认就是YES,设置为NO的话,如果没有set或_set的方法,那么就无法对实例变量进行赋值
+(BOOL)accessInstanceVariablesDirectly
{
return YES;
}
#pragma mark - KVC - setKey流程
//这就是文档中说的set<key>
- (void)setName:(NSString *)name
{
NSLog(@"%s---%@",__func__,name);
}
//_set<key>
- (void)_setName:(NSString *)name
{
NSLog(@"%s---%@",__func__,name);
}
- (void)setIsName:(NSString *)name
{
NSLog(@"%s---%@",__func__,name);
}
@end
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
JDPerosn *person = [[JDPerosn alloc] init];
//1. KVC - 设置值的过程
[person setValue:@"LJD" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,
person->name,person->isName);
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
NSLog(@"%@-%@",person->name,person->isName);
NSLog(@"%@",person->isName);
}
执行结果 :
图1.0.1.png
可以再把- (void)setName:(NSString *)name
方法注释掉,会发现调用- (void)_setName:(NSString *)name
,如果把它也注释掉,则会调用- (void)setIsName:(NSString *)name
。4个实例变量
的值依然都是null
。
这就验证了上面说的第一点,在利用KVC
设置值的时候,会先找到set
或者_set
方法对其进行赋值。
(2). 验证上面的第二点 :
把JDPerson.m
中的有关set
的3个方法全部注释掉,然后让accessInstanceVariablesDirectly
变成return NO
。其余代码不变,再执行。结果如下图 :
所以验证了第二点中,没有set
或_set
方法的时候,accessInstanceVariablesDirectly
要设置成YES
才可以。
然后将accessInstanceVariablesDirectly
重新设置成YES
。再执行。结果如下图 :
验证了第二点中会先给_name
赋值。
然后依次的注释掉JDPerson.h
中的成员变量_name
、_isName
、name
。
就会验证第二点中,KVC
赋值同名变量的顺序是_name
、_isName
、name
、isName
。
第三点我就不验证了,因为经常会用到,比如字典转模型的时候,大家应该常用。
另外,KVC
为什么有第二点这种设计呢?因为在编译期的时候,底层也会生成一些这样的变量,比如之前我们在前面的章节进行clang
将.m
文件编译成.cpp
文件的时候,有一些属性就会变成了_xxx
的形式的成员变量,或者BOOL
类型的值会变成isXXX
的形式,这也是因为运行时的特性造成的。
总结 :
KVC设值流程.png二、KVC的访问模式——取值过程
一样先来查阅官方文档,然后根据自己的理解加上翻译,再做总结。
图2.0.0.png明显getter
的过程比较多。还是按照官方的分成六点。
【前言】 : valueForKey:
的默认实现,给定key
作为输入参数,执行下列程序,从接收valueForKey:
调用的类实例的内部操作。
- 第一,按照顺序查找,是否有
get<Key>, <key>, is<Key>,_<key>
这些方法。如果找到了就调用它,并且在下面的第五点
中处理结果。如果找不到就进入第二点
。
- 第二,如果
第一点
中的4个get
方法都没实现,在实例方法中查找countOf<Key>
、objectIn<Key>AtIndex:
还有<key>AtIndexes:
方法。
- 如果
countOf<Key>
存在,并且objectIn<Key>AtIndex:
和<key>AtIndexes:
中至少一个方法存在,那么创建一个响应所有的NSArray
方法的集合代理对象
,并且返回这个集合代理对象
。- 否则进入
第三点
。集合代理对象
随后会将所有接收到的NSArray
的消息都转换为countOf
、objectInAtIndex:
和AtIndexes:
的一些组合,这些组合将消息发送给创建它的符合KVC
机制的对象。- 如果原始对象还实现了一个名为
get:range:
的可选方法,代理对象也会在适当的时候使用它。- 实际上,代理对象与和
KVC
兼容的对象一起工作,允许底层属性像NSArray
一样工作,即使它不是NSArray
。
- 第三,如果没有找到
第二点
中的3个方法,则同时查找countOf <Key>,enumeratorOf<Key>和memberOf<Key>
这三个方法,
- 如果这三个方法都找到了,创建一个响应所有
NSSet
方法的集合代理对象
,并将集合代理对象
返还。- 如果这三个方法也没找到,那么就直接进入
第四点
。集合代理对象
随后将它接收到的任何NSSet消息转换为countOf
、enumeratorOf
和memberOf:
消息的组合,并发送给创建它的对象。- 实际上,代理对象与遵循
KVC
的对象一起工作,允许底层属性像NSSet
一样运行,即使它不是NSSet
。
- 第四,如果
get
方法和集合方法
都没找到,并且接收者的类方法accessinstancevariables
返回YES,则按照顺序查找_<key>, _is<Key>, <key>, is<Key>
变量。如果找到,直接获取实例变量的值并进入第五点
。否则,进入第六点
。
- 第五,根据搜索到的属性值的类型,返回不同的结果
- 如果是
对象指针
,则直接返回结果。- 如果值是
NSNumber
支持的标量类型,将其存储在NSNumber
实例中并返回。- 如果结果是
NSNumber
不支持的标量类型,转换为NSValue对象
并返回它。
- 第六,如果上面的所有方法都没有找到,调用
setValue:forUndefinedKey:
。默认情况下会抛出异常,可以执行这个方法来处理。
因为这里明显的能看出来,简单类型取值和集合类型是不一样的,先看简单类型,按照步骤,举个例子 :
(1). 验证上面的第1点和第4点 :
JDPerson.h
不发生改变。
JDPerson.m
#pragma mark - 开启或关闭实例变量的赋值
//默认就是YES,设置为NO的话,如果没有set或_set的方法,那么就无法对实例变量进行赋值
+(BOOL)accessInstanceVariablesDirectly
{
return YES;
}
#pragma mark - KVC - getKey流程
//文档中说的get<key>
- (NSString *)getName
{
return NSStringFromSelector(_cmd);
}
//\<key>
- (NSString *)name
{
return NSStringFromSelector(_cmd);
}
//is<key>
- (NSString *)isName
{
return NSStringFromSelector(_cmd);
}
//_<key>
- (NSString *)_name
{
return NSStringFromSelector(_cmd);
}
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self jd_kvc_getter];
}
- (void)jd_kvc_getter
{
JDPerosn *person = [[JDPerosn alloc] init];
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"KVC取值---%@",[person valueForKey:@"name"]);
}
- (void)jd_kvc_setter
{
JDPerosn *person = [[JDPerosn alloc] init];
//1. KVC - 设置值的过程
[person setValue:@"LJD" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,
person->name,person->isName);
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
NSLog(@"%@-%@",person->name,person->isName);
NSLog(@"%@",person->isName);
}
执行结果 :
图2.0.1.png然后依次注释掉4个get
方法,就可以验证第一点,并且全部注释掉以后会走到第四点,顺便把第四点也验证了。
从验证可以看出,
valueForKey
先找的不是我们对变量的赋值,而是先找的getter
方法,只有getter
方法没有实现的情况下,才会找到变量直接拿值。简单类型
也就是非集合类型的
只会找到第一点
和第四点
如果有错误会直接到第六点
。
(1). 验证上面的第2点和第3点 :
因为第二,第三点的原理是一样的,这里就拿更常见的数组来说明,NSSet
的例子会放出来,但是就不说明了。
JDPerson.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface JDPerosn : NSObject
{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
NSArray *arr;
NSSet *set;
}
@end
NS_ASSUME_NONNULL_END
JDPerson.m
#pragma mark - KVC - 正常的数组取值
- (NSUInteger)countOfArr
{
NSLog(@"%s",__func__);
return [arr count];
}
- (id)objectInArrAtIndex:(NSUInteger)index
{
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"objectInArrAtIndex : %lu",index];
}
#pragma mark - KVC - 正常的集合取值
- (NSUInteger)countOfSet
{
NSLog(@"%s",__func__);
return [set count];
}
- (id)memberOfSet:(id)object
{
NSLog(@"%s",__func__);
return [set containsObject:object] ? object : nil;
}
- (id)enumeratorOfSet
{
NSLog(@"%s",__func__);
return [set objectEnumerator];
}
#pragma mark - KVC - 没有Pens,但是我有方法,一样可以取值数组
- (NSUInteger)countOfPens
{
NSLog(@"%s",__func__);
return [arr count];
}
- (id)objectInPensAtIndex:(NSUInteger)index
{
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"objectInPensAtIndex %lu", index];
}
- (id)pensAtIndexes:(NSUInteger)index
{
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"pensAtIndexes %lu", index];
}
#pragma mark - KVC - 没有books,但是我有方法,一样可以取值集合
// 个数
- (NSUInteger)countOfBooks{
NSLog(@"%s",__func__);
return [arr count];
}
// 是否包含这个成员对象
- (id)memberOfBooks:(id)object {
NSLog(@"%s",__func__);
return [set containsObject:object] ? object : nil;
}
// 迭代器
- (id)enumeratorOfBooks {
// objectEnumerator
NSLog(@"来了 迭代编译");
return [arr reverseObjectEnumerator];
}
ViewController.m
- (void)jd_kvc_array_and_set
{
JDPerosn *person = [[JDPerosn alloc] init];
person->arr = @[@"pen0", @"pen1", @"pen2", @"pen3"];
NSLog(@"正常的数组取值 : %@",[person valueForKey:@"arr"]);
NSArray *array = [person valueForKey:@"pens"];
NSLog(@"就算没有pens,只要在类中实现了方法也可以objectAtIndex : %@",[array objectAtIndex:1]);
NSLog(@"就算没有pens,只要在类中实现了方法也可以containsObject : %d",[array containsObject:@"pen1"]);
person->set = [NSSet setWithArray:person->arr];
NSLog(@"正常的集合取值 : %@",[person valueForKey:@"set"]);
NSSet *set = [person valueForKey:@"books"];
[set enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"set遍历 %@",obj);
}];
}
执行结果就不贴图了,太长了,可以自己运行一下,会发现count
的方法都会打印两次,也证明了的确是存在着代理集合的。所以验证也是符合的。
总结 :
kvc-getter简单类型流程图.png
KVC
的valueForKey
的流程主要还是分了简单对象和集合对象,简单对象的取值和其setValueForKey
是一样的。而集合对象则多需要几步骤,但是可以防止取到不认识的key
,也可以修改取值的结果。
三、KVC的一些特殊功能
1. KVC有自动转换类型的功能。
JDPerson.h
:
typedef struct {
float x, y, z;
} ThreeFloats;
@interface JDPerosn : NSObject
{
@public
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
NSArray *arr;
NSSet *set;
int age;
ThreeFloats threeFloats;
}
@end
1.1 NSNumber
支持的标量类型
//1. NSNumber支持的标量类型
JDPerosn *person = [[JDPerosn alloc] init];
[person setValue:@18 forKey:@"age"];
NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
[person setValue:@"20" forKey:@"age"];
NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
图3.1.0.png
证明了 :
KVC
在对已知类型的value
设置的时候,如果类型是NSNumber
支持的标量类型,则会将value
存储到NSNumber
的实例中,在取值的时候返回NSNumber
实例。
1.2 NSNumber不支持的标量类型
//2. NSNumber不支持的标量类型
ThreeFloats floats = {1.f, 2.f, 3.f};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue
图3.1.1.png
证明了 :
KVC
存储的类型如果是NSNumber
不支持的标量类型,那么就要转换为NSValue
存储并且返回。
2. 空值
在JDPerson.m
中添加KVC
监控 :
- (void)setNilValueForKey:(NSString *)key{
NSLog(@"设置 %@ 是空值",key);
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"没有这个key : %@",key);
}
- (id)valueForUndefinedKey:(NSString *)key{
NSLog(@"没有这个key : %@ - 给你一个其他的吧,别奔溃了!",key);
return @"LJD";
}
2.1 key
存在,value
设置为空
在-(void)viewDidLoad
中 :
JDPerosn *person = [[JDPerosn alloc] init];
[person setValue:nil forKey:@"age"]; // subject不会走 - 官方注释里面说只对 NSNumber - NSValue
[person setValue:nil forKey:@"name"];
图3.2.0.png
图3.2.1.png
证明了 :
KVC
中的setNilValueForKey
只监控NSNumber
和NSValue
的结构体,不会监控到其他类型。
2.2 key
不存在,value
设置为空
[person setValue:nil forKey:@"LJD"]; //LJD不是已知的key
可以实现这个 :
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
进行监控。
图3.2.2.png2.3 key
为空
NSLog(@"%@",[person valueForKey:@"KC"]); //KC是不存在的key
这个可以使用 :
- (id)valueForUndefinedKey:(NSString *)key
进行监控。
图3.2.3.png3. 键值验证
JDPerson.m
中添加 :
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
if([inKey isEqualToString:@"name"]){
[self setValue:[NSString stringWithFormat:@"可以修改一下: %@",*ioValue] forKey:inKey];
return YES;
}
*outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:16688 userInfo:nil];
return NO;
}
viewDidLoad
中 :
NSError *error;
NSString *name = @"LJD";
JDPerosn *person = [[JDPerosn alloc] init];
if (![person validateValue:&name forKey:@"names" error:&error]) {
NSLog(@"%@",error);
}else{
NSLog(@"%@",[person valueForKey:@"name"]);
}
结果 :
图3.3.0.png
网友评论