iOS Objective-C KVC 详解
1. KVC简介
KVC 全称Key Value Coding,是苹果两大开发语言Objective-C
和Swift
中的一个重要概念,翻译过来就是键值编码。详情可以在官方文档中进行查看。
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.
【译】 键值编码是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象符合键值编码时,可通过简洁,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。
KVC常见用法:
iOS Objective-C KVC 的常见用法
2. KVC 取值设值原理
2.1 Basic Getter(基本 getter)
valueForKey:
方法在调用者传入key
之后会在对象中按下列步骤进行搜索:
- 以
get
,
,is
,_
的顺序查找对象中是否有对应的方法。- 如果找到了,就调用这个方法并执行步骤5
- 如果没找到则进行下一步
- 在对象中搜索
countOf
和objectIn
(类似于AtIndex: NSArray
类中的对应的方法),还有
(类似于AtIndexes: NSArray
中的objectsAtIndexes:
方法)- 如果找到其中的第一个
countOf
,再找到其他两个中的至少一个,则创建一个响应所有NSArray
方法的的代理集合对象,并将其返回,代理对象随后将任何NSArray接收到的一些组合的消息countOf
和,objectIn AtIndex:
消息给键-值编码创建它兼容的对象。如果原始对象还实现了名称为的可选方法AtIndexes: get
,则代理对象也会在适当时使用该方法。实际上,代理对象与与键值编码兼容的对象一起使用,可以使基础属性的行为就好像它是:range: NSArray
,即使不是。 - 如果没有找到,请继续执行步骤3。
- 如果找到其中的第一个
- 查找命名为
countOf
,enumeratorOf
和memberOf
这三个方法(对应于: NSSet
定义的原始方法)。- 如果找到这三个方法,请创建一个响应的所有
NSSet
方法的代理集合对象,并返回该对象。随后,这个代理对象将它接收到的任何NSSet
消息转换为countOf
、enumeratorOf
和memberOf
消息的组合,并发送给创建它的对象。实际上,代理对象与遵循键值编码的对象一起工作,允许底层属性像: NSSet
一样运行,即使它不是NSSet
。 - 如果找不到,请继续执行步骤4。
- 如果找到这三个方法,请创建一个响应的所有
- 判断类方法
accessInstanceVariablesDirectly
- 如果返回YES,则以
_
、_is
、
、is
这个顺序进行查找。如果找到,就可以直接获取实例变量的值,然后继续执行步骤5。 - 如果找不到,请继续执行步骤6。
- 如果返回YES,则以
- 判断取出的属性值
- 如果属性值是对象,则直接返回。
- 如果不是对象,并且属性值该值可以被
NSNumber
包装,则将其存储在NSNumber
实例中返回 - 如果结果是
NSNumber
不支持的标量类型,就转换为NSValue
对象进行返回
- 到了这步就是所有方法均失败,请调用
valueForUndefinedKey:
。默认情况下会引发异常,但是NSObject
的子类可以重写该方法进行特定的处理。
以上步骤可以通过简单的流程图来表示:
2.2 Basic Setter(基本 setter)
在调用setValue:forKey:
方法给定的key
和value
参数作为输入,尝试设置命名属性key
的value
(或者对于非对象属性的展开值)的时候按照如下步骤进行设置:
- 首先按照
set
、_set
、setIs
的顺序查找方法,如果找到就使用传入的value
作为参数,并调用找到的方法,并完成操作。 - 如果没找到方法就来到此步骤,如果类方法
accessInstanceVariablesDirectly
返回YES
,则寻找一个实例变量,按照_
、_is
、
、is
的顺序查找,如果找到就将输入值(或者展开值)设置变量并完成操作。 - 如果没有找到实例变量就会调用
setValue:forUndefinedKey:
方法,默认情况下会引发异常,但是NSObject
的子类可以重写该方法进行相应的处理。
以上步骤可以通过简单的流程图来表示:
2.3 Mutable Arrays(可变数组)
在调用mutableArrayValueForKey:
方法,给定key
参数作为输入的情况下,默认使用以下过程返回一个可变数组:
- 寻找一个名字叫做
insertObject:in
和AtIndex: removeObjectFrom
(对应AtIndex: NSMutableArray
的原始方法insertObject:atIndex:
和removeObjectAtIndex:
)或者 名字叫做insert
和:atIndexes: remove
(对应AtIndexes: NSMutableArray
中的insertObjects:atIndexes:
andremoveObjectsAtIndexes:
方法)- 如果对象至少有一个插入方法和一个删除方法(需同时包含插入和删除)此时就可以返回一个包含
insertObject:in
、AtIndex: removeObjectFrom
、AtIndex: insert
、:atIndexes: remove
方法的AtIndexes: NSMutableArray
代理对象来响应mutableArrayValueForKey:
- 当对象接受到
mutableArrayValueForKey:
消息时也可以实现一个可选的替换对象方法,该方法名称类似以replaceObjectIn
或者AtIndex:withObject: replace
代理对象也会在适当的时候使用这些方法以获得最佳性能。AtIndexes:with :
- 如果对象至少有一个插入方法和一个删除方法(需同时包含插入和删除)此时就可以返回一个包含
- 如果没有找到可变数组的方法,则查找名称为
set
的方法,在这种情况下,返回一个: NSMutableArray
代理对象,通过向set
的原始接收者发出消息来响应: mutableArrayValueForKey:
的调用。(此步骤效率很低,它可能涉及重复创建新的集合对象,而不是修改,在设计自己KVC
的时候应该尽量避免使用它) - 如果上面的也不成功则判断
accessInstanceVariablesDirectly
返回YES
,然后按照_
、
的顺序查找一个实例变量,如果找到则返回一个代理对象,这个代理对象将NSMutableArray
接收到的每个消息转发到该实例变量的值,该值通常是NSMutableArray
的子类的实例或者子类之一。 - 如果所有方法均失败,则返回一个可变的收集代理对象,该对象将在收到消息时调用
setValue:forUndefinedKey:
向mutableArrayValueForKey:
消息的原始接收者发出NSMutableArray
消息。setValue:forUndefinedKey:
的默认实现引发一个NSUndefinedKeyException
,子类可以重写进行容错处理。
2.4 Mutable Ordered Sets(可变有序集合)
在调用mutableOrderedSetValueForKey:
方法后,它的默认实现是触发简单的方法识别器和有序的方法识别器(详见2.1 Basic Getter),它还遵循跟以上相同的实例变量访问策略,但是总是返回一个可变集合的代理对象,而不是valueForKey:
返回的不可变集合,此外它还做了以下工作:
- 查找名称为
insertObject:in
和AtIndex: removeObjectFrom
(与AtIndex: NSMutableOrderedSet
定义的原始方法相对应),以及insert
和:atIndexes: remove
(与AtIndexes: insertObjects:atIndexes:
和removeObjectsAtIndexes:
对应)- 如果发现至少一个插入方法和至少一个删除方法,返回一个包含
insertObject:in
、AtIndex: removeObjectFrom
、AtIndex: insert
、:atIndexes: remove
方法的AtIndexes: NSMutableOrderedSet
代理对象,来响应mutableOrderedSetValueForKey:
消息的原始接收者。 - 当名称为
replaceObjectIn
或者AtIndex:withObject: replace
存在于原始对象中时,这个代理对象也可以使用这些方法。AtIndexes:with :
- 如果发现至少一个插入方法和至少一个删除方法,返回一个包含
- 如果没有找到任何可变的设置方法,那么搜索名称类似于
set
的方法,在这种情况下返回的代理对象在每次接收到: NSMutableOrderedSet
消息时,都会发生一个set
的消息给: mutableOrderedSetValueForKey:
的原始接收者。此步骤的效率也远低于前一步,因为它可能涉及重复创建新的集合对象,而不是修改现有的集合对象,因此在我们自己设计使用KVC
时应该避免使用它。 - 如果既没有找到可变的集合也没有找到访问器的方法,那么如果接收方的
accessInstanceVariablesDirectly
类方法返回YES
则按照_
、
的顺序查找实例变量,如果找到了这样的实例变量,则返回的代理对象会将NSMutableOrderedSet
收到的所有消息转发到该实例变量的值,该值通常是NSMutableOrderedSet
其子类的一个实例或其中一个子类。 - 如果所有其他方法均失败,则返回的代理对象每当收到可变设置消息时都会
setValue:forUndefinedKey:
向原始接收者发送一条mutableOrderedSetValueForKey:
消息。 默认的实现setValue:forUndefinedKey:
引发一个NSUndefinedKeyException
,但是对象可以重写进行容错处理。
2.5 Mutable Sets(可变集合)
在调用mutableSetValueForKey:
后给定一个key
参数作为输入,在默认情况下通过如下过程为接收访问器调用的对象内部命名的数组属性返回一个可变的代理集合。
- 首先搜索方法名称为
add
和Object: remove
的方法(对应Object: NSMutableSet
内的addObject:
和removeObject:
方法)和add
和: remove
方法(对应: NSMutableSet
内部的unionSet:
和minusSet:
方法)- 如果找到了至少一个添加方法和至少一个删除方法,就返回一个包含
add
、Object: remove
、Object: add
、: remove
方法的: NSMutableSet
类型的代理对象来响应mutableSetValueForKey:
消息的原始接收者。 - 如果这个代理对象的
intersect
或者: set
方法可用,我们也可以使用这些方法来获得最佳性能。:
- 如果找到了至少一个添加方法和至少一个删除方法,就返回一个包含
- 如果
mutableSetValueForKey:
调用的接收者是一个托管对象,则搜索模式不会像对非托管对象那样继续。 - 如果未找到可变集合设置方法,并且该对象不是托管对象,则查找
set
方法,如果找到了就返回一个代理对象,返回的代理对象在每次接收到: NSMutableOrderedSet
消息时,都会发送一个set
的消息给: mutableOrderedSetValueForKey:
的原始接收者。此步骤中描述的机制的效率远低于第一步,因为它可能涉及重复创建新的集合对象,而不是修改现有的集合对象。因此,在设计自己的KVC
对象时,通常应该避免使用它。 - 如果没有找到可变集合的适配器方法,判断
accessinstancevariablesdirect
类方法是否返回YES
,如果是YES
则按照_
、
的顺序搜索这样的实例变量,如果找到了实例变量代理对象将它接收到的每个NSMutableSet
消息转发给实例变量的值,该值通常是NSMutableSet
的一个实例或它的一个子类。 - 如果以上步骤都失败了,返回的代理对象通过发送
setValue:forUndefinedKey:
消息给mutableSetValueForKey:
的原始接收方,来响应它接收到的任何NSMutableSet
消息。
2.6 小结
以上我们常用的就是基本的getter和基本的setter,剩下的不常用,分析的比较粗糙,如有错误欢迎指正。
3. 自定义KVC
了解了KVC
底层原理之后,我们尝试自己实现一个简单的KVC
。我们点击跳转到KVC
声明的地方发现这是声明在分类的方法,这种设计模式可以实现解耦的功能。简单来说就是如果都写在一起也可以,但是使用分类就会显得功能单一清晰,维护方便,就跟我们在开发中集成各种第三方框架需要在AppDelegate
里面注册时一样,时间久了集成的多了就会不断的堆积,致使文件越来越大,不容易维护,这个时候使用这种设计模式将不同的注册和初始化区分开来,就会减轻文件的代码量,同时也就减轻了维护成本,使整个业务流程更加清晰。
所以我们新建一个NSObject
的分类,简单的定义两个方法,代码如下:
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (CustomKVC)
- (nullable id)c_valueForKey:(NSString *)key;
- (void)c_setValue:(nullable id)value forKey:(NSString *)key;
@end
NS_ASSUME_NONNULL_END
3.1 自定义Setter
- 首先我们需要做一些非空判断,如果传入的
key
为空就直接返回:
// 1:非空判断一下
if (key == nil || key.length == 0) return;
- 然后根据我们前面探索的
setValue:ForKey:
的流程,我们按照set
、_set
、setIs
的顺序判断是否能够响应方法的实现,如果可以响应就调用响应方法来设置属性值,然后结束该流程。
// 2:找到相关方法 set _set setIs
// 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 custom_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self custom_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self custom_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
custom_performSelectorWithMethodName
方法判断能否有方法响应,如果可以就调用。
- (BOOL)custom_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;
}
- 这里我们简单的实现
KVC
就先忽略掉集合的相关处理,我们直接来到accessInstanceVariablesDirectly
类方法返回值的判断,如果返回NO
就抛出异常
// -- MARK: 省略集合相关的处理
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"CustomUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
- 如果返回
YES
就按照_
、_is
、
、is
的顺序查找实例变量。如果查找到了就直接给实例变量赋值,然后return。
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _ _is is
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
// 判断是否包含顺序中的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;
}
获取当前对象拥有的实例变量的方法getIvarListName
- (NSMutableArray *)getIvarListName{
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
// 获取当前类的所有成员变量
Ivar *ivars = class_copyIvarList([self class], &count);
// 变量所有成员变量
for (int i = 0; i
关于getIvarListName
方法中用到的两个Runtime
的API
:
class_copyIvarList:
/**
* Describes the instance variables declared by a class.
*
* @param cls The class to inspect.
* @param outCount On return, contains the length of the returned array.
* If outCount is NULL, the length is not returned.
*
* @return An array of pointers of type Ivar describing the instance variables declared by the class.
* Any instance variables declared by superclasses are not included. The array contains *outCount
* pointers followed by a NULL terminator. You must free the array with free().
*
* If the class declares no instance variables, or cls is Nil, NULL is returned and *outCount is 0.
*/
OBJC_EXPORT Ivar _Nonnull * _Nullable
class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
返回类结构中成员变量的指针数组,但不包括父类中声明的成员变量。该数组包含*outCount
指针,后吗跟一个NULL
终止符号。使用完毕后需要通过free()
函数对其释放,如果该类未声明任何实例变量或者cls
为Nil
,则返回NULL
,并*outCount
为0。
ivar_getName:
/* Working with Instance Variables */
/**
* Returns the name of an instance variable.
*
* @param v The instance variable you want to enquire about.
*
* @return A C string containing the instance variable's name.
*/
OBJC_EXPORT const char * _Nullable
ivar_getName(Ivar _Nonnull v)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
返回实例变量的名称。
- 最后就是到了都处理不了的时候了,我们直接抛出异常
// 5:如果找不到相关实例
@throw [NSException exceptionWithName:@"CustomUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
3.2 自定义Getter
- 首先我们还是做一下非空的判断,如果没有
key
就不需要查了。
// 1:非空判断
if (key == nil || key.length == 0) { return nil; }
- 然后我们根据前面探索过得
valueForKey:
的流程首先按照get
,
,is
,_
的顺序查找对象中是否有对应的方法。如果找到就直接调用返回即可。
// 2:找到相关方法 `get`,``,`is`,`_`
// key 要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
NSString *_key = [NSString stringWithFormat:@"_%@:",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(isKey)]){
return [self performSelector:NSSelectorFromString(isKey)];
}else if ([self respondsToSelector:NSSelectorFromString(_key)]){
return [self performSelector:NSSelectorFromString(_key)];
}
#pragma clang diagnostic pop
- 如果并没有找到以上的方法则需要判断一些集合的处理方法,这里咱们就先忽略了,我们直接判断类方法
accessInstanceVariablesDirectly
的返回值,如果不是YES
则抛出异常。
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"CustomUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
- 如果返回
YES
就按照_
、_is
、
、is
的顺序查找实例变量,如果找到了就直接返回实例变量的值。
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// `_`、`_is`、``、`is`
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);;
}
- 最后就是都没找到就抛出异常。
// 5.抛出异常
@throw [NSException exceptionWithName:@"CustomUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: valueForUndefinedKey:%@.****",self,NSStringFromSelector(_cmd),key] userInfo:nil];
3.3小结
至此我们自定义KVC
就结束了,当然跟真正的KVC
实现还相差甚远,我们没有对集合类型做处理,也没有对非对象类型进行展开赋值和取值的包装,也没有根据不同的值类型调用不同的异常处理方法,而是直接抛出异常,同样也没有做线程安全的加锁处理,但是自己实现一个简单的 KVC
有助于我们对KVC
底层操作步骤的的深入理解。
DIS_KVC_KVO一个根据iOS Foundation
框架汇编反写的KVC
,KVO
实现。
4. 总结
至此我们的KVC
就探索完毕了,这篇文章的内容基本都来源于苹果的官方文档About Key-Value Coding,本文对KVC
的描述也不是很完善,那么最最完善就是官方文档了,如有不理解不明白的多多查看官方文档一定会有新的收获。
-
KVC
是一种NSKeyValueCoding
的非正式协议机制 -
KVC
主要通过valueForKey:
和valueForKeyPath:
两个方法进行取值 -
KVC
主要通过setValue:ForKey:
和setValue:ForKeyPath:
两个方法进行设置值 - 对于集合类型的对象还要进行特殊的处理
- 对于非对象类型的
value
可以通过NSNumber
和NSValue
进行包装
5.参考资料
Apple Documentation - About Key-Value Coding