KVC是咱们在平常开发中经常使用的功能,俗称'键值编码',本章节就来探索一下咱们经常使用的KVC究竟是如何实现的html
KVC
全称是Key-Value Coding
,键值编码,能够经过Key来访问和修改属性。ios
咱们能够经过查看Apple的官方文档来查看其定义和具体的用法。git
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.github
[译]键值编码是由
NSKeyValueCoding
非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合键值编码时,能够经过简洁,统一的消息传递接口经过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。编程
经过文档,咱们知道了以下几点:api
get
和set
方法的监听来进行取值和设值基本的用法分为两种:赋值和取值。主要API以下数组
//赋值 - (void)setValue:(id)value forKeyPath:(NSString *)keyPath; - (void)setValue:(id)value forKey:(NSString *)key; 复制代码
//取值
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
复制代码
首先先创建一个LGPerson
类,经过对该类各不一样类型的属性的赋值来说解,代码以下bash
typedef struct { float x, y, z; } ThreeFloats; @interface LGPerson : NSObject{ @public NSString *myName; } @property (nonatomic, copy) NSString *name; @property (nonatomic, strong) NSArray *array; @property (nonatomic, strong) NSMutableArray *mArray; @property (nonatomic, assign) int age; @property (nonatomic) ThreeFloats threeFloats; @property (nonatomic, strong) LGStudent *student; @end 复制代码
基本对象的使用是平时开发中最为经常使用的,就如类中的name
属性,定义为public
的myName
成员变量和基本类型int
的age
属性,直接使用上述的基本用法赋值便可,代码以下markdown
[person setValue:@"WY" forKey:@"name"]; [person setValue:@19 forKey:@"age"]; [person setValue:@"WYY" forKey:@"myName"]; NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]); -------------------打印结果------------------------ WY - 19 - WYY 复制代码
层级使用也比较容易理解,主要使用了setValue:forKeyPath:
的相关方法。好比要对类中student
属性中的相关属性进行赋值的时候,就不能直接进行赋值,而须要使用点语法来进行层级赋值,具体代码以下多线程
LGStudent *student = [[LGStudent alloc] init]; student.subject = @"iOS"; person.student = student; [person setValue:@"ios" forKeyPath:@"student.subject"]; NSLog(@"%@",[person valueForKeyPath:@"student.subject"]); -------------------打印结果------------------------ ios 复制代码
集合类型的使用即为Person
对象中数组类型的属性进行赋值。上述例子中,因为属性是不可变数组,咱们是不能直接对该属性进行赋值操做的,通常状况下咱们须要引入中间变量来进行等价替换,相关代码以下
person.array = @[@"1",@"2",@"3"]; // 因为不是可变数组 - 没法作到 // person.array[0] = @"100"; NSArray *array = [person valueForKey:@"array"]; // 用 array 的值建立一个新的数组 array = @[@"100",@"2",@"3"]; [person setValue:array forKey:@"array"]; NSLog(@"%@",[person valueForKey:@"array"]); -------------------打印结果------------------------ ( 100, 2, 3 ) 复制代码
能够发现,若是使用中间变量来进行操做的话,步骤仍是相对繁琐一些的,咱们可使用mutableArrayValueForKey
方法来达到简化的目的,相关代码以下
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"]; ma[0] = @"100"; NSLog(@"%@",[person valueForKey:@"array"]); -------------------打印结果------------------------ ( 100, 2, 3 ) 复制代码
经过上述的两种方法,咱们能够看到使用KVC方式来对集合进行可变方法操做的读写更加的便捷高效,主要的方法以下
mutableArrayValueForKey:
和 mutableArrayValueForKeyPath:
返回的代理对象表现为一个 NSMutableArray
对象
mutableSetValueForKey:
和 mutableSetValueForKeyPath:
返回的代理对象表现为一个 NSMutableSet
对象
mutableOrderedSetValueForKey:
and mutableOrderedSetValueForKeyPath:
返回的代理对象表现为一个 NSMutableOrderedSet
对象
这一部分在平时的开发中可能使用较少,主要是在valueForKeyPath
时,进行一些譬如求和,平均值等操做高效运算来使用的。
主要分为如下三大类:
聚合操做符
@avg
: 返回操做对象指定属性的平均值@count
: 返回操做对象指定属性的个数@max
: 返回操做对象指定属性的最大值@min
: 返回操做对象指定属性的最小值@sum
: 返回操做对象指定属性值之和数组操做符
@distinctUnionOfObjects
: 返回操做对象指定属性的集合--去重@unionOfObjects
: 返回操做对象指定属性的集合嵌套操做符
@distinctUnionOfArrays
: 返回操做对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray@unionOfArrays
: 返回操做对象(集合)指定属性的集合@distinctUnionOfSets
: 返回操做对象(嵌套集合)指定属性的集合--去重,返回的是 NSSet以下图所示,当属性为基本标量属性时,能够经过NSNumber
的相关方法,转换为对象类型的NSNumber
来进行对应的读写操做
如图所示,咱们常见的结构体的类型以下,能够经过先转换为NSValue
类型的对象属性,而后来进行对应的读写操做
Person
类中有一个自定义结构体的属性,咱们对它来进行赋值看看,相关的代码以下
ThreeFloats floats = {1., 2., 3.}; NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)]; [person setValue:value forKey:@"threeFloats"]; NSValue *reslut = [person valueForKey:@"threeFloats"]; NSLog(@"%@",reslut); ThreeFloats th; [reslut getValue:&th] ; NSLog(@"%f - %f - %f",th.x,th.y,th.z); -------------------打印结果------------------------ {length = 12, bytes = 0x0000803f0000004000004040} 1.000000 - 2.000000 - 3.000000 复制代码
咱们能够看到,在存储时,咱们队结构体进行了编码,并生成了NSValue
的变量,进行存储。而后又从NSValue
提取到了相对应结构体的值,完成了读写的流程。
KVC
支持属性验证,而这一特性是经过validateValue:forKey:error:
(或validateValue:forKeyPath:error:
) 方法来实现的。这个验证方法的默认实现是去收到这个验证消息的对象(或keyPath中最后的对象)中根据 key 查找是否有对应的 validate<Key>:error:
方法实现,若是没有,验证默认成功,返回 YES。
而因为 validate<Key>:error:
方法经过引用接收值和错误参数,因此会有如下三种结果:
验证方法认为值对象有效,并在YES不更改值或错误的状况下返回。
验证方法认为值对象无效,但选择不对其进行更改。在这种状况下,该方法返回N
O错误参考并将错误参考(若是由调用者提供)设置到一个NSError
指示失败缘由的对象。
验证方法认为值对象无效,但建立了一个新的有效对象做为替换。在这种状况下,该方法返回,YES而错误对象保持不变。在返回以前,该方法将值引用修改成指向新值对象。进行修改时,即便值对象是可变的,该方法也老是建立一个新对象,而不是修改旧对象。
相关的代码以下
Person* person = [[Person alloc] init]; NSError* error; NSString* name = @"John"; if (![person validateValue:&name forKey:@"name" error:&error]) { NSLog(@"%@",error); } 复制代码
经过官方文档,咱们能够很清晰的知道KVC在底层是如何进行set
和get
的
set<Key>
、_set<Key>
、setIs<Key>
的方法。若是存在,则直接进行调用accessInstanceVariablesDirectly
方法是否为YES
(改方法系统默认为YES,用来判断是都容许对成员变量的赋值)。若是为YES
。则按照顺序,查找成员变量_Key
、_isKey
、Key
、isKey
,若是存在对应的成员变量,则直接进行赋值,若是不存在,则进行下一步3.setValue:forUndefinedKey:
报错图解以下
以 get<Key>
, <key>
, is<Key>
以及 _<key>
的顺序查找对象中是否有对应的方法。
查找是否有 countOf<Key>
和 objectIn<Key>AtIndex:
方法(对应于 NSArray 类定义的原始方法)以及 <key>AtIndexes:
方法(对应于 NSArray 方法 objectsAtIndexes:)
countOf<Key>
),再找到其余两个中的至少一个,则建立一个响应全部 NSArray 方法的代理集合对象,并返回该对象。(翻译过来就是要么是 countOf<Key> + objectIn<Key>AtIndex:
,要么是 countOf<Key> + <key>AtIndexes:
,要么是 countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:
)查找名为 countOf<Key>
,enumeratorOf<Key>
和 memberOf<Key>
这三个方法(对应于NSSet
类定义的原始方法)
NSSet
方法的代理集合对象,并返回该对象判断类方法 accessInstanceVariablesDirectly
结果
YES
,则以_<key>
, _is<Key>
, <key>
, is<Key>
的顺序查找成员变量,若是找到了,将成员变量带上跳转到第 5 步,若是没有找到则跳转到第 6 步NO
,跳转到第 6 步判断取出的属性值
NSNumber
类型,则将属性值转化为 NSNumber
类型返回NSNumber
类型,则将属性值转化为 NSValue
类型返回调用 valueForUndefinedKey:
。 默认状况下,这会引起一个异常,可是NSObject
的子类能够提供特定于 key 的行为。
图解以下:
了解了KVC的基本使用和底层原理以后,咱们能够根据其底层原理,来实现一个简单的自定义KVC。
首先咱们建立一个NSObject
的分类并添加前缀自定义方法名,用来解耦和避免和系统方法冲突
根据上面的小结,主体思路以下:
set<Key>
、 _set<Key>
、setIs<Key>
相关代码以下:
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{ ✅// 1:非空判断一下 if (key == nil || key.length == 0) return; ✅// 2:找到相关方法 set<Key> _set<Key> setIs<Key> ✅// key 要大写 NSString *Key = key.capitalizedString; ✅ // 拼接方法 NSString *setKey = [NSString stringWithFormat:@"set%@:",Key]; NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key]; NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key]; if ([self lg_performSelectorWithMethodName:setKey value:value]) { NSLog(@"*********%@**********",setKey); return; }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) { NSLog(@"*********%@**********",_setKey); return; }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) { NSLog(@"*********%@**********",setIsKey); return; } ✅// 3:判断是否可以直接赋值实例变量 if (![self.class accessInstanceVariablesDirectly] ) { @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; } ✅// 4.找相关实例变量进行赋值 ✅// 4.1 定义一个收集实例变量的可变数组 NSMutableArray *mArray = [self getIvarListName]; // _<key> _is<Key> <key> is<Key> NSString *_key = [NSString stringWithFormat:@"_%@",key]; NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key]; NSString *isKey = [NSString stringWithFormat:@"is%@",Key]; if ([mArray containsObject:_key]) { ✅// 4.2 获取相应的 ivar Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String); ✅// 4.3 对相应的 ivar 设置值 object_setIvar(self , ivar, value); return; }else if ([mArray containsObject:_isKey]) { Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String); object_setIvar(self , ivar, value); return; }else if ([mArray containsObject:key]) { Ivar ivar = class_getInstanceVariable([self class], key.UTF8String); object_setIvar(self , ivar, value); return; }else if ([mArray containsObject:isKey]) { Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String); object_setIvar(self , ivar, value); return; } ✅// 5:若是找不到相关实例 @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil]; } 复制代码
根据上面的小结,主体思路以下:
get<Key>
、 <key>
、countOf<Key>
、objectIn<Key>AtIndex
相关的代码以下:
- (nullable id)lg_valueForKey:(NSString *)key{ ✅// 1: 判断非空 if (key == nil || key.length == 0) { return nil; } ✅// 2:找到相关方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex ✅// key 要大写 NSString *Key = key.capitalizedString; ✅// 拼接方法 NSString *getKey = [NSString stringWithFormat:@"get%@",Key]; NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key]; NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" if ([self respondsToSelector:NSSelectorFromString(getKey)]) { return [self performSelector:NSSelectorFromString(getKey)]; }else if ([self respondsToSelector:NSSelectorFromString(key)]){ return [self performSelector:NSSelectorFromString(key)]; }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){ if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) { int num = (int)[self performSelector:NSSelectorFromString(countOfKey)]; NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1]; for (int i = 0; i<num-1; i++) { num = (int)[self performSelector:NSSelectorFromString(countOfKey)]; } for (int j = 0; j<num; j++) { id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)]; [mArray addObject:objc]; } return mArray; } } #pragma clang diagnostic pop ✅// 3:判断是否可以直接赋值实例变量 if (![self.class accessInstanceVariablesDirectly] ) { @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil]; } ✅// 4.找相关实例变量进行赋值 ✅// 4.1 定义一个收集实例变量的可变数组 NSMutableArray *mArray = [self getIvarListName]; // _<key> _is<Key> <key> is<Key> // _name -> _isName -> name -> isName NSString *_key = [NSString stringWithFormat:@"_%@",key]; NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key]; NSString *isKey = [NSString stringWithFormat:@"is%@",Key]; if ([mArray containsObject:_key]) { Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String); return object_getIvar(self, ivar);; }else if ([mArray containsObject:_isKey]) { Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String); return object_getIvar(self, ivar);; }else if ([mArray containsObject:key]) { Ivar ivar = class_getInstanceVariable([self class], key.UTF8String); return object_getIvar(self, ivar);; }else if ([mArray containsObject:isKey]) { Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String); return object_getIvar(self, ivar);; } return @""; } 复制代码
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{ if ([self respondsToSelector:NSSelectorFromString(methodName)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self performSelector:NSSelectorFromString(methodName) withObject:value]; #pragma clang diagnostic pop return YES; } return NO; } - (id)performSelectorWithMethodName:(NSString *)methodName{ if ([self respondsToSelector:NSSelectorFromString(methodName)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" return [self performSelector:NSSelectorFromString(methodName) ]; #pragma clang diagnostic pop } return nil; } - (NSMutableArray *)getIvarListName{ NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1]; unsigned int count = 0; Ivar *ivars = class_copyIvarList([self class], &count); for (int i = 0; i<count; i++) { Ivar ivar = ivars[i]; const char *ivarNameChar = ivar_getName(ivar); NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar]; NSLog(@"ivarName == %@",ivarName); [mArray addObject:ivarName]; } free(ivars); return mArray; } 复制代码
这里只是很是简单的实现,不少多线程等状况没有考虑,能够参考DIS_KVC_KVO的实现方式,来进行进一步加深印象