iOS Objective-C KVC 详解

iOS Objective-C KVC 详解

1. KVC简介

KVC 全称Key Value Coding,是苹果两大开发语言Objective-CSwift中的一个重要概念,翻译过来就是键值编码。详情可以在官方文档中进行查看。

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之后会在对象中按下列步骤进行搜索:

  1. getis_的顺序查找对象中是否有对应的方法。
    • 如果找到了,就调用这个方法并执行步骤5
    • 如果没找到则进行下一步
  2. 在对象中搜索countOfobjectInAtIndex:(类似于NSArray类中的对应的方法),还有AtIndexes:(类似于NSArray中的objectsAtIndexes:方法)
    • 如果找到其中的第一个countOf,再找到其他两个中的至少一个,则创建一个响应所有NSArray方法的的代理集合对象,并将其返回,代理对象随后将任何NSArray接收到的一些组合的消息countOf,objectInAtIndex:AtIndexes:消息给键-值编码创建它兼容的对象。如果原始对象还实现了名称为的可选方法get:range:,则代理对象也会在适当时使用该方法。实际上,代理对象与与键值编码兼容的对象一起使用,可以使基础属性的行为就好像它是NSArray,即使不是。
    • 如果没有找到,请继续执行步骤3。
  3. 查找命名为countOfenumeratorOfmemberOf:这三个方法(对应于NSSet定义的原始方法)。
    • 如果找到这三个方法,请创建一个响应的所有NSSet方法的代理集合对象,并返回该对象。随后,这个代理对象将它接收到的任何NSSet消息转换为countOfenumeratorOfmemberOf:消息的组合,并发送给创建它的对象。实际上,代理对象与遵循键值编码的对象一起工作,允许底层属性像NSSet一样运行,即使它不是NSSet
    • 如果找不到,请继续执行步骤4。
  4. 判断类方法accessInstanceVariablesDirectly
    • 如果返回YES,则以__isis这个顺序进行查找。如果找到,就可以直接获取实例变量的值,然后继续执行步骤5。
    • 如果找不到,请继续执行步骤6。
  5. 判断取出的属性值
    • 如果属性值是对象,则直接返回。
    • 如果不是对象,并且属性值该值可以被NSNumber包装,则将其存储在NSNumber实例中返回
    • 如果结果是NSNumber不支持的标量类型,就转换为NSValue对象进行返回
  6. 到了这步就是所有方法均失败,请调用valueForUndefinedKey:。默认情况下会引发异常,但是NSObject的子类可以重写该方法进行特定的处理。

以上步骤可以通过简单的流程图来表示:

Basic Getter.jpg

2.2 Basic Setter(基本 setter)

在调用setValue:forKey:方法给定的keyvalue参数作为输入,尝试设置命名属性keyvalue(或者对于非对象属性的展开值)的时候按照如下步骤进行设置:

  1. 首先按照set_setsetIs的顺序查找方法,如果找到就使用传入的value作为参数,并调用找到的方法,并完成操作。
  2. 如果没找到方法就来到此步骤,如果类方法accessInstanceVariablesDirectly返回YES,则寻找一个实例变量,按照__isis的顺序查找,如果找到就将输入值(或者展开值)设置变量并完成操作。
  3. 如果没有找到实例变量就会调用setValue:forUndefinedKey:方法,默认情况下会引发异常,但是NSObject的子类可以重写该方法进行相应的处理。

以上步骤可以通过简单的流程图来表示:

KVC Basic Setter.jpg

2.3 Mutable Arrays(可变数组)

在调用mutableArrayValueForKey:方法,给定key参数作为输入的情况下,默认使用以下过程返回一个可变数组:

  1. 寻找一个名字叫做insertObject:inAtIndex:removeObjectFromAtIndex:(对应NSMutableArray的原始方法insertObject:atIndex:removeObjectAtIndex:)或者 名字叫做insert:atIndexes:removeAtIndexes: (对应NSMutableArray中的insertObjects:atIndexes: and removeObjectsAtIndexes:方法)
    1. 如果对象至少有一个插入方法和一个删除方法(需同时包含插入和删除)此时就可以返回一个包含insertObject:inAtIndex:removeObjectFromAtIndex:insert:atIndexes:removeAtIndexes:方法的NSMutableArray代理对象来响应mutableArrayValueForKey:
    2. 当对象接受到mutableArrayValueForKey:消息时也可以实现一个可选的替换对象方法,该方法名称类似以replaceObjectInAtIndex:withObject: 或者 replaceAtIndexes:with:代理对象也会在适当的时候使用这些方法以获得最佳性能。
  2. 如果没有找到可变数组的方法,则查找名称为set:的方法,在这种情况下,返回一个NSMutableArray代理对象,通过向set:的原始接收者发出消息来响应mutableArrayValueForKey:的调用。(此步骤效率很低,它可能涉及重复创建新的集合对象,而不是修改,在设计自己KVC的时候应该尽量避免使用它)
  3. 如果上面的也不成功则判断accessInstanceVariablesDirectly返回YES,然后按照_的顺序查找一个实例变量,如果找到则返回一个代理对象,这个代理对象将NSMutableArray接收到的每个消息转发到该实例变量的值,该值通常是NSMutableArray的子类的实例或者子类之一。
  4. 如果所有方法均失败,则返回一个可变的收集代理对象,该对象将在收到消息时调用setValue:forUndefinedKey:mutableArrayValueForKey:消息的原始接收者发出NSMutableArray消息。setValue:forUndefinedKey:的默认实现引发一个NSUndefinedKeyException,子类可以重写进行容错处理。

2.4 Mutable Ordered Sets(可变有序集合)

在调用mutableOrderedSetValueForKey:方法后,它的默认实现是触发简单的方法识别器和有序的方法识别器(详见2.1 Basic Getter),它还遵循跟以上相同的实例变量访问策略,但是总是返回一个可变集合的代理对象,而不是valueForKey:返回的不可变集合,此外它还做了以下工作:

  1. 查找名称为insertObject:inAtIndex:removeObjectFromAtIndex:(与NSMutableOrderedSet定义的原始方法相对应),以及insert:atIndexes:removeAtIndexes:(与insertObjects:atIndexes:removeObjectsAtIndexes:对应)
    1. 如果发现至少一个插入方法和至少一个删除方法,返回一个包含insertObject:inAtIndex:removeObjectFromAtIndex:insert:atIndexes:removeAtIndexes:方法的NSMutableOrderedSet代理对象,来响应mutableOrderedSetValueForKey:消息的原始接收者。
    2. 当名称为replaceObjectInAtIndex:withObject:或者replaceAtIndexes:with:存在于原始对象中时,这个代理对象也可以使用这些方法。
  2. 如果没有找到任何可变的设置方法,那么搜索名称类似于set:的方法,在这种情况下返回的代理对象在每次接收到NSMutableOrderedSet消息时,都会发生一个set:的消息给mutableOrderedSetValueForKey:的原始接收者。此步骤的效率也远低于前一步,因为它可能涉及重复创建新的集合对象,而不是修改现有的集合对象,因此在我们自己设计使用KVC时应该避免使用它。
  3. 如果既没有找到可变的集合也没有找到访问器的方法,那么如果接收方的accessInstanceVariablesDirectly类方法返回YES则按照_的顺序查找实例变量,如果找到了这样的实例变量,则返回的代理对象会将NSMutableOrderedSet收到的所有消息转发到该实例变量的值,该值通常是NSMutableOrderedSet其子类的一个实例或其中一个子类。
  4. 如果所有其他方法均失败,则返回的代理对象每当收到可变设置消息时都会setValue:forUndefinedKey:向原始接收者发送一条mutableOrderedSetValueForKey:消息。 默认的实现setValue:forUndefinedKey:引发一个NSUndefinedKeyException,但是对象可以重写进行容错处理。

2.5 Mutable Sets(可变集合)

在调用mutableSetValueForKey:后给定一个key参数作为输入,在默认情况下通过如下过程为接收访问器调用的对象内部命名的数组属性返回一个可变的代理集合。

  1. 首先搜索方法名称为addObject:removeObject:的方法(对应NSMutableSet内的addObject:removeObject:方法)和add:remove:方法(对应NSMutableSet内部的unionSet:minusSet:方法)
    1. 如果找到了至少一个添加方法和至少一个删除方法,就返回一个包含addObject:removeObject:add:remove:方法的NSMutableSet类型的代理对象来响应mutableSetValueForKey:消息的原始接收者。
    2. 如果这个代理对象的intersect:或者set: 方法可用,我们也可以使用这些方法来获得最佳性能。
  2. 如果mutableSetValueForKey:调用的接收者是一个托管对象,则搜索模式不会像对非托管对象那样继续。
  3. 如果未找到可变集合设置方法,并且该对象不是托管对象,则查找set:方法,如果找到了就返回一个代理对象,返回的代理对象在每次接收到NSMutableOrderedSet消息时,都会发送一个set:的消息给mutableOrderedSetValueForKey:的原始接收者。此步骤中描述的机制的效率远低于第一步,因为它可能涉及重复创建新的集合对象,而不是修改现有的集合对象。因此,在设计自己的KVC对象时,通常应该避免使用它。
  4. 如果没有找到可变集合的适配器方法,判断accessinstancevariablesdirect类方法是否返回YES,如果是YES则按照_的顺序搜索这样的实例变量,如果找到了实例变量代理对象将它接收到的每个NSMutableSet消息转发给实例变量的值,该值通常是NSMutableSet的一个实例或它的一个子类。
  5. 如果以上步骤都失败了,返回的代理对象通过发送setValue:forUndefinedKey:消息给mutableSetValueForKey:的原始接收方,来响应它接收到的任何NSMutableSet消息。

2.6 小结

以上我们常用的就是基本的getter和基本的setter,剩下的不常用,分析的比较粗糙,如有错误欢迎指正。

3. 自定义KVC

了解了KVC底层原理之后,我们尝试自己实现一个简单的KVC。我们点击跳转到KVC声明的地方发现这是声明在分类的方法,这种设计模式可以实现解耦的功能。简单来说就是如果都写在一起也可以,但是使用分类就会显得功能单一清晰,维护方便,就跟我们在开发中集成各种第三方框架需要在AppDelegate里面注册时一样,时间久了集成的多了就会不断的堆积,致使文件越来越大,不容易维护,这个时候使用这种设计模式将不同的注册和初始化区分开来,就会减轻文件的代码量,同时也就减轻了维护成本,使整个业务流程更加清晰。

NSObject(NSKeyValueCoding).jpg

所以我们新建一个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_setsetIs的顺序判断是否能够响应方法的实现,如果可以响应就调用响应方法来设置属性值,然后结束该流程。
    // 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就按照__isis的顺序查找实例变量。如果查找到了就直接给实例变量赋值,然后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方法中用到的两个RuntimeAPI

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()函数对其释放,如果该类未声明任何实例变量或者clsNil,则返回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:的流程首先按照getis_的顺序查找对象中是否有对应的方法。如果找到就直接调用返回即可。
    // 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就按照__isis的顺序查找实例变量,如果找到了就直接返回实例变量的值。
    // 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的描述也不是很完善,那么最最完善就是官方文档了,如有不理解不明白的多多查看官方文档一定会有新的收获。

  1. KVC是一种NSKeyValueCoding的非正式协议机制
  2. KVC主要通过valueForKey:valueForKeyPath:两个方法进行取值
  3. KVC主要通过setValue:ForKey:setValue:ForKeyPath:两个方法进行设置值
  4. 对于集合类型的对象还要进行特殊的处理
  5. 对于非对象类型的value可以通过NSNumberNSValue进行包装

5.参考资料

Apple Documentation - About Key-Value Coding

你可能感兴趣的:(iOS Objective-C KVC 详解)