KVC原理探究

KVC

iOS在实际开发过程中用KVC的地方也是不少的,但是很少有时间探究里面涵盖的内容,网上的一些文章也纯属是翻译的官方文档。有些细节根本没有验证完全, 本文着重探索一下里面的实现细节。

KVC.png

一、KVC 定义

       KVC全称Key Value Coding,具体来说就是KVC是由NSKeyValueCoding非正式协议启用的机制,对象采用该机制提供对其属性的间接访问。当对象符合键值编码时(通常在OC中,继承NSObject即可),其属性可通过字符串参数通过简洁,统一的消息传递接口来存取消息。它属于间接访问对象的属性,区别于属性的直接访问方法settergetter,亦或直接访问成员变量.因为是间接访问属性,所以KVC性能比不上直接存取属性的方法,但是可以提高程序的灵活性。必要的时候可以减少冗余代码。相关参考KVC官方文档

二、NSKeyValueCoding

图片.png

三、基本操作

KVC对属性的操作主要分为:
基础属性--标量(整型,浮点型等),字符串,布尔类型,NSNumber, NSColor等
一对一关系的属性--当前对象有一个对象属性,对象属性在改变自己内部属性的时候,对象属性不会改变
一对多关系的属性--通过NSArray或NSSet包含的其它对象集合

3.1 基础属性

//银行账户
@interface BankAccount : NSObject

@property (nonatomic) NSNumber* currentBalance;              // An attribute 
@property (nonatomic) Person* owner;                         // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation

@end

使用属性字符串作key,来查找当前的属性

//存取普通属性 An attribute
NSLog(@"currentBalance--%@", [self.account valueForKey:@"currentBalance"]);//通过key获取属性
[self.account setValue:@(111) forKey:@"currentBalance"];                   //通过key设置属性值
NSLog(@"currentBalance--%@", [self.account valueForKey:@"currentBalance"]);//通过key获取属性

3.2 一对一关系的属性和Key Paths

// 个人信息
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;

@end

可以通过key path 获取BankAccount对象中的属性person,进而获取Personname属性

//一对一属性 to - one
NSLog(@"name--%@", [self.account valueForKeyPath:@"owner.name"]); //通过keypath获取属性
[self.account setValue:@"NewName" forKeyPath:@"owner.name"];      //通过keypath设置属性值
NSLog(@"name--%@", [self.account valueForKeyPath:@"owner.name"]); //通过keypath获取属性

       所谓Key Paths 是指由一串点分隔键组成的字符串,用于指定要遍历的对象属性序列。字符串序列中的第一个键的属性是相对于接收者的(owner是 self.account的属性,接收者是self.account),子序列中的第一个键的属性是相对于前一个属性的(name是owner的属性)

3.3 一对多关系的属性

       当给键值编码兼容对象发送valueForKeyPath:消息时,可以在键路径中嵌入一个集合运算符。 集合运算符是一个小符号列表之一,前面是一个符号(@),它指定了getter在返回之前应该以某种方式操作数据的操作。 NSObject提供的valueForKeyPath:的默认实现实现了这种行为。其格式如下:

keypath.jpg
  • @collectionOperator左侧是Left key path,可以省略(如果当属性对象是NSarray时)
  • @collectionOperator左侧是Right key path,除了@count集合操作符,其它的Right key path必须存在

3.3.1 Aggregation Operators聚合操作符

       聚合运算符以某种方式合并集合的对象,并返回通常与右键路径中指定的属性的数据类型匹配的单个对象。 @count运算符是一个例外 - 它没有正确的键路径,总是返回一个NSNumber实例。

  • @avg
NSNumber *transactionAverage = [self.account.transactions valueForKeyPath:@"@avg.amount"];
  • @count
NSNumber *numberOfTransactions = [self.account.transactions valueForKeyPath:@"@count"];
  • @max
NSDate *latestDate = [self.account.transactions valueForKeyPath:@"@max.date"];
  • @min
NSDate *earliestDate = [self.account.transactions valueForKeyPath:@"@min.date"];
  • @sum
NSNumber *amountSum = [self.account.transactions valueForKeyPath:@"@sum.amount"];

3.3.2 Array Operators数组操作符

数组操作符通过调用valueForKeyPath和right key path 指示的对象返回一个特定的序列

  • @distinctUnionOfObjects 返回一个不重复元素的Array
NSArray *distinctPayees = [self.account.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
  • @unionOfObjects 返回一个所有元素的Array
NSArray *distinctPayees = [self.account.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

3.3.3 Nesting Operators嵌套操作符

嵌套操作作用在一个嵌套的序列上,序列中的每一个元素也是一个序列。

  • @distinctUnionOfArrays 返回一个不重复元素的Array
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
  • @unionOfArrays 返回一个所有元素的Array
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
  • @distinctUnionOfSets 返回一个不重复元素的Set
NSSet *set1 = [NSSet setWithArray:self.account.transactions];
NSSet *set2 = [NSSet setWithArray:moreTransactions];
NSSet *set = [NSSet setWithObjects:set1, set2, nil];
NSSet *collectedSetPayees = [set valueForKeyPath:@"@distinctUnionOfSets.payee"];
下面是基本的存取方法的简介:
  • valueForKey:根据给定的key获取当前对象对应属性的value
  • valueForKeyPath:根据给定的keyPath获取当前对象路径上对应属性的value,如果路径中有一个key不匹配的话,就会调用 valueForUndefinedKey:
  • dictionaryWithValuesForKeys:将当前对象所有的属性和其值,封装成一个字典返回,内部调用valueForKey:
  • setValue:forKey:根据给定的key设置当前对象对应属性的value
  • setValue:forUndefinedKey:如果指定的key在当前对象的属性中无法匹配的话,就会调用
  • setValue:forKeyPath:根据给定的keyPath设置当前对象路径上对应属性的value,如果路径中有一个key不匹配的话,就会调用setValue:forUndefinedKey
  • setValuesForKeysWithDictionary:利用字典设置当前对象的属性值,字典中的每个key,对应当前对象中的一个属性,内部实现是多次调用setValue:forKey
  • mutableArrayValueForKey: and mutableArrayValueForKeyPath:返回一个可变的数组对象进行操作(像NSMutableArray)
  • mutableSetValueForKey: and mutableSetValueForKeyPath:返回一个可变的Set对象进行操作(像NSMutableSet)
  • mutableOrderedSetValueForKey: and mutableOrderedSetValueForKeyPath:返回一个可变的有序的Set对象进行操作(像NSMutableOrderedSet)

注: 非对象属性通过KVC设置值,如果值nil,会调用setNilValueForKey:,其内部触发一个异常,可以根据需要重写它来进行其它操作

四、非对象类型的表示

KVC支持scalar和结构体,对于KVC中的value,必须是一个对象类型,所以scalar和结构体必须包裹成相应的对象。

  • scalar类型可以使用NSNumber或者简写@()
[self.account setValue:[NSNumber numberWithInt:10] forKey:@"no"];
[self.account setValue:@(10) forKey:@"no"];
  • 结构体可以使用NSValue包裹
typedef struct {
float x, y, z;
} ThreeFloats;

NS_ASSUME_NONNULL_BEGIN

@interface StructValueTest : NSObject

@property (nonatomic) ThreeFloats threeFloats;

@end

StructValueTest *myClass = [[StructValueTest alloc] init];
NSValue *result = [myClass valueForKey:@"threeFloats"]; 
ThreeFloats temp;
[result getValue:&temp];   //通过getValue获取真实的类型
NSLog(@"修改前x-%f,y-%f,z-%f", temp.x, temp.y, temp.z);

ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"]; //设置新值
result = [myClass valueForKey:@"threeFloats"]; 
[result getValue:&temp];   //通过getValue获取真实的类型
NSLog(@"修改前x-%f,y-%f,z-%f", temp.x, temp.y, temp.z);

五、属性的验证

Key-value coding protocol支持属性验证,但是不会自动地调用验证方法(如果自动调用肯定会消耗性能),必须根据自己的需求在合适的时机调用 validateValue:forKey:error:。但是默认的方法始终返回Yes,所以无法验证,必须写一个validate<Key>:error:方法也实现具体的验证思路。

  • 如果不写validate<Key>:error:方法,下面的验证无效
Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
NSLog(@"%@",error);
}
  • 验证validateName:error:
- (BOOL)validateName:(id *)ioValue error:(NSError **)outError {
//这里只判断类型是不是NSString类型,是返回true,否返回false
NSString *result = (NSString *)*ioValue;
if ([result isKindOfClass:[NSString class]]) {
return true;
}
NSError *error = [[NSError alloc] initWithDomain:@"0" code:100 userInfo:@{@"info":@"type error"}];
*outError = error;
return false;
}
//属性验证
- (void)validateProperty {
Person* person = [[Person alloc] init];
NSError* error;
//    NSString* name = @"John";
NSNumber *num = [NSNumber numberWithInt:12];
if (![person validateValue:&num forKey:@"name" error:&error]) {
NSLog(@"%@",error);
}
}

验证结果:
2019-04-29 01:41:08.969260+0800 KVC[6294:243510] Error Domain=0 Code=100 "(null)" UserInfo={info=type error}

六、基本Getter方法的搜索模式

NSObject提供的NSKeyValueCoding协议的默认实现使用明确定义的一套规则将基于Key的访问器的调用映射到对象的内部属性。也就是说内部有自己的一套访问存取方法,实例变量和其它的一些相关方法。
这是valueForKey:的默认实现,给定一个key当做输入参数,开始下面的步骤,在这个接收valueForKey:方法调用的类内部进行操作。

  • 1通过getter方法搜索实例,搜索顺序为get<Key>, <key>, is<Key>, _get<Key>,_<key>方法
  • 2如果相应的存取方法没找到,那么查找下面四个方法countOf<Key>,get<Key>:range:,objectIn<Key>AtIndex:, <key>AtIndexes:,第一个方法实现的情况下,剩下的三个方法实现其一就行,上面三个方法的顺序就是他们的调用优先级,此过程是响应NSArray对应的方法(看样子只是对NSArray类型有实际作用),如果上面相应的方法没有实现进入步骤3
  • 3 查找countOf<Key>, enumeratorOf<Key>, and memberOf<Key>:方法,此过程是响应NSSet对应的方法(看样子只是对NSSet类型有实际作用),如果没有找到进入步骤4
  • 4如果第3步还是没有找到,且accessInstanceVariablesDirectly是返回YES的。
  • 5根据顺序搜索一个名为_<key>_is<Key><key>is<Key>的实例变量,然后返回。
    如果取回的是一个对象指针,则直接返回这个结果。
    如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。
    如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
  • 6如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法,处理异常或什么也不操作

搜索路径如下图:

搜索路径.png

七、基本Setter搜索模式

这是setValue:forKey:的默认实现,根据给定的key设置相应的value

  • 查找set<Key>:_set<Key>命名的setter,按照这个顺序,如果找到的话,调用这个方法并将值传进去(根据需要进行对象转换)。
  • 如果没有发现一个简单的setter,但是accessInstanceVariablesDirectly类属性返回YES,则查找一个命名规则为_<key>_is<Key><key>is<Key>的实例变量。根据这个顺序,如果发现则将value赋值给实例变量。
    如果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认抛出一个异常,子类可以重写,指定自己的行为
    搜索路径如下图:
    图片.png

八、可变数组搜索模式

mutableArrayValueForKey:根据给定的key返回一个可变的value(可变数组)
mutableArrayValueForKey:内部调用顺序如下:

  • 1实现可变数组方法insertObject:in<Key>AtIndex:removeObjectFrom<Key>AtIndex: (对应NSMutableArray中的insertObject:atIndex:and removeObjectAtIndex:)这对操作与insert<Key>:atIndexes:remove<Key>AtIndexes:(对应NSMutableArray中的insertObjects:atIndexesremoveObjectsAtIndexes:)这对操作从中选一个insert操作和一个remove操作即可。
    如果还实现了操作replaceObjectIn<Key>AtIndex:withObject:replace<Key>AtIndexes:with<Key>:,代理对象会在适当的情况下使用它们,以获得最佳性能
  • 2如果没有实现可变数组方法,那么会找相应的存取器方法set<Key>:方法和<key>方法(这个点和官方文档不太一致,实测只有同时实现setter方法和getter方法才会正常运行,否则崩溃).set方法的搜索按set<Key>:_set<Key>这个顺序,get方法的搜索按get<Key>, <key>, is<Key>, _get<Key>,_<key>这个顺序
  • 3如果第2步还是没有找到,且accessInstanceVariablesDirectly是返回YES的。执行第4步
  • 4按顺序搜索一个名为_<key>_is<Key><key>is<Key>的实例变量,然后返回。
  • 5如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法,处理异常或什么也不操作

搜索路径如下图:

图片.png

九、可变有序Set搜索模式

mutableOrderedSetValueForKey:根据给定的key返回一个可变的value(有序可变Set)
其实现流程和可变数组大致一致,不再赘述,可看demo

十、可变Set搜索模式

mutableSetValueForKey:,根据给定的key返回一个可变的value(可变Set)
其实现流程和可变数组大致一致,这里只给一个流程图,具体可查看demo

图片.png

十一、KVC其它操作

  • Key Paths
    OC中的key paths 必须是一个字符串,字样就会造成一定的写错机率,
    可以借助runtime的一些方法[object valueForKey:NSStringFromSelector(@selector(method))];但是这样势必造成一定的性能损失,怎么取舍看自己的情况。
    对于swift 4中新增了keypath的操作,它要比OC中一keypath更加灵活。不再使用字符串,这样就可以防止写错的情况。
class User: NSObject{
var name:String = ""  
var age:Int = 0 
}
let user = User()
user.name = "hangge"
user.age = 100
//取值
let name = user1[keyPath: \User.name]
print(name)
//设置值
user1[keyPath: \User.name] = "hangge.com"
  • 修改私有属性的值
    OC语言中没有绝对的私有,我们可以通过runtime获取相应的私有属性,然后可根据需要修改,比如:
    KVC可以给对象的私有变量赋值(UIPageControl)
  • 字典转模型
    setValuesForKeysWithDictionary:
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342