1.简介
先来看苹果文档的一段介绍:
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.
简单翻译一下:
键值编码是一种由NSKeyValueCoding非正式协议启用的机制,对象采用该协议来提供对其属性的间接访问。当对象符合键值编码时,可以通过简洁、统一的消息传递接口通过字符串参数对其属性进行寻址。这种间接访问机制补充了实例变量及其关联的访问方法所提供的直接访问。
通常使用访问器方法来访问对象的属性。get访问器(或getter)返回属性的值。set访问器(或setter)设置属性的值。在Objective-C中,你也可以直接访问一个属性的底层实例变量。以上述任何一种方式访问对象属性都很简单,但需要调用特定于属性的方法或变量名。随着属性列表的增长或变化,访问这些属性的代码也必须变化。相反,与键值编码兼容的对象提供了一个简单的消息传递接口,该接口在其所有属性之间保持一致。
键值编码是许多其他Cocoa技术的基础概念,如键值观察、Cocoa绑定、核心数据和AppleScript-ability。在某些情况下,键值编码还可以帮助简化代码。
我们主要探索一下KVC的底层原理和使用.
2.KVC原理
1.Setter的设值过程
- 首先:先找setter,先查找set:方法,如果没有,再找_set方法,这两个方法按顺序查找,找到任何一个则完成赋值.
- 再次:如果上面两个方法都未找到,并且类方法accessInstanceVariablesDirectly返回YES,则查找一个名称依次为_、_is< key>、、 is的实例变量,如果找到任何一个则完成赋值.(如果该类方法返回NO,则会出现一个常见的崩溃:this class is not key value coding-compliant for the key name.)
- 其次,按照以上顺序没有找到setter或实例变量时,则会调用setValue:forUndefinedKey:在默认情况下会引发异常.
2.Getter的取值过程
valueForKey:的默认实现:给定一个键参数作为输入,执行以下过程,在类实例中操作,接收valueForKey:调用。
- 1.在实例中,按照方法名get,,is,_< Key>的顺序搜索方法,如果找到任何一个,调用它并继续执行第5步。否则继续下一步。
- 以下步骤是找NSArray & NSSet集合方法
- 2.如果没有找到1步骤中的简单方法,那么在实例中搜索名称与模式countOf方法,并且至少找到了objectInAtIndex:(对应于NSArray类定义的基本方法)和 AtIndexes:(对应于NSArray方法objectsAtIndexes:)匹配的方法中的一个。那么创建一个响应所有NSArray方法的集合代理对象并返回它。否则,继续执行步骤3。
- 代理对象随后将它接收到的任何NSArray消息转换为某种组合计数(countOf, objectInAtIndex:, and AtIndexes: messages)到创建它的符合键值编码的对象。如果原始对象还实现了一个名为get:range:的可选方法,那么代理对象也会在适当的时候使用该方法。实际上,代理对象与键值编码兼容对象一起工作,允许底层属性的行为就像它是NSArray一样,即使它不是NSArray。
- 3.如果没有找到简单的访问器方法或数组访问方法组,则查找名为countOf、enumeratorOf和memberOf:(对应于NSSet类定义的基元方法)的三种方法。
- 如果找到了这三个方法,创建一个集合代理对象来响应所有NSSet方法并返回它。否则,继续执行步骤4。
- 这个代理对象随后将它接收到的任何NSSet消息转换成某种组合:countOf、enumeratorOf和memberOf: messages to the object that created it。实际上,代理对象与键值编码兼容对象一起工作,允许底层属性的行为就像NSSet一样,即使它不是NSSet。
- 2、3步骤如果没有找到,那么开始找实例变量.
- 4.如果没有找到简单的访问器方法或集合访问方法组,并且如果接收方的类方法accessInstanceVariablesDirectly返回YES,则按该顺序搜索名为_、_is< key>、、is的实例变量。如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。
- 5.如果检索到的属性值是一个对象指针,则只需返回结果。如果值是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回该值。如果结果是不受NSNumber支持的标量类型,转换成NSValue对象并返回它。
- 6.如果其他方法都失败,调用valueForUndefinedKey:方法,默认情况下会引发异常。
3.自定义KVC
- NSObject+WMKVCSetting.h
@interface NSObject (WMKVCSetting) //设值 - (void)wm_setValue:(nullable id)value forKey:(NSString *)key; //取值 - (nullable id)wm_valueForKey:(NSString *)key; @end
- NSObject+WMKVCSetting.m
#import "NSObject+WMKVCSetting.h" #import <objc/runtime.h> @implementation NSObject (WMKVCSetting) - (void)wm_setValue:(nullable id)value forKey:(NSString *)key{ // 1 非空判断一下 if (key == nil || key.length == 0) return; // 2 找到相关方法 set<Key> _set<Key> setIs<Key> // 2.1 key的首字母大写 NSString *Key = key.capitalizedString; //2.2 拼接方法名 NSString *setKey = [NSString stringWithFormat:@"set%@:",Key]; NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key]; NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key]; //2.3 统一方法处理,判断能否相应方法设值 if ([self wm_performSelectorWithMethodName:setKey value:value]) { NSLog(@"%@",setKey); return; }else if ([self wm_performSelectorWithMethodName:_setKey value:value]) { NSLog(@"%@",_setKey); return; }else if ([self wm_performSelectorWithMethodName:setIsKey value:value]) { NSLog(@"%@",setIsKey); return; } // 3 判断是否能够直接赋值实例变量 accessInstanceVariablesDirectly类方法是否返回YES. if (![self.class accessInstanceVariablesDirectly] ) { @throw [NSException exceptionWithName:@"WMUnknownKeyException" reason:[NSString stringWithFormat:@"reason:[%@ 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]; //4.2 判断获取相应的 ivar 并对相应的 ivar 设置值 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; }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:@"WMUnknownKeyException" reason:[NSString stringWithFormat:@"reason:[%@ %@]: this class is not key value coding-compliant for the key name.",self,NSStringFromSelector(_cmd)] userInfo:nil]; } - (nullable id)wm_valueForKey:(NSString *)key{ // 1 刷选key 判断非空 if (key == nil || key.length == 0) { return nil; } // 2 找到相关方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex // 2.1 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:@"WMUnknownKeyException" reason:[NSString stringWithFormat:@"reason:[%@ 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 @""; } #pragma mark - 相关方法 - (BOOL)wm_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; } //获取Ivar列表 - (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; } @end