OC底层原理17 - KVC

简介

KVC的全称是Key-Value Coding,翻译成中文是键值编码,键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象是键值编码兼容的对象时,可以通过简洁,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。

常用API

//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;

//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;

//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; 

//通过KeyPath来设值                 
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;

//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;

//和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

KVC 设值、取值流程探索

这里主要依据官网描述和代码调试配合完成探索。

  • setValue:forKey:
    将给定的key和value参数作为输入,尝试在接收调用的对象中将名为key的属性的值设置为value。设置步骤如下:
    【第一步】查找是否存在指定的setter方法,查找顺序是:
    1. set<Key>:
    2. _set<Key>:
    3. setIs<Key>:
    这三个方法,若存在其中任意一个,则执行该方法,在该方法中可以设置属性的值,若不存在,则进入【第二步】。
    注意:KVO设值过程中不会查找和调用_setIs<Key>函数。
    【第二步】检查类方法InstanceVariablesDirectly的返回值。
    YES:查看是否有指定的成员变量,查找的顺序是:
    1. _<Key>
    2. _is<Key>
    3. <Key>
    4. is<Key>
    若存在其中任意一个,则向那个成员变量赋值。若不存在则进入【第三步】。
    NO:则进入【第三步】
    【第三步】执行setValue:forUndefinedKey:方法,该方法一般情况是抛出NSUndefinedKeyException类型的异常。可以在应用中重写该方法。
    KVO设值流程如下图所示:

    KVO设值流程

  • valueForKey:
    在给定key参数作为输入的情况下,尝试从接收调用的对象中获取名为key的属性的值。获取值步骤如下:
    【第一步】查找是否存在指定的getter方法,查找顺序是:
    1. get<Key>
    2. <Key>
    3. is<Key>
    4. _<Key>
    如果找到这4个方法中的一个,则调用它并进入【第五步】。否则,继续下一步。
    【第二步】查找countOf<Key>以及objectIn<Key>AtIndex:<Key>AtIndexes:方法。

    如果找到countOf<Key>和其它两个方法中的一个,则会返回一个可以响应NSArray所有方法代理集合(它是NSKeyValueArray,是NSArray的子类)。或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex:<Key>AtIndexes:这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法。

    如果不能返回代理集合,即未找到指定的方法,请继续进入【第三步】。

    【第三步】同时查找countOf<Key>enumeratorOf<Key>,memberOf<Key>这三个方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>enumeratorOf<Key>,memberOf<Key>组合的形式调用。

    【第四步】如果还没有找到,检查类方法InstanceVariablesDirectly的返回值。
    如果为YES,则查找指定的成员变量,如果找到指定的成员变量,则返回该成员变量。查找顺序为:
    1. _<Key>
    2. _is<Key>
    3. <Key>
    4. is<Key>
    如果为NO,则进入【第六步】

    【第五步】根据搜索到的属性值类型,返回不同的结果:
    * 如果是对象,则直接返回。
    * 如果是NSNumber支持的标量类型,则将其包装成NSNumber对象并返回它。
    * 如果是NSNumber不支持的标量类型,则将其转换为NSValue对象并返回它。

    【第六步】执行valueForUndefinedKey:方法,该方法一般情况是抛出NSUndefinedKeyException类型的异常。可以在应用中重写该方法。

自定义实现KVC

自定义 KVC 设值

既然已经知道KVC的设值流程,那接下来就模仿系统接口,自定义一个KVC设值接口hq_setValue:forKey:

  1. 异常判断,当key为空时,则不继续操作。
  2. 由于KVC设值是先查找setter方法,因此,先根据查找的顺序定义setter方法名称。
  3. 通过respondsToSelector函数判断当前方法是否存在,若存在,则执行方法并在执行后退出接口。
  4. setter方法不存在,判断是否响应accessInstanceVariablesDirectly方法,即间接访问实例变量,返回YES,继续下一步设值;返回NO,则抛出异常。
  5. 间接访问变量赋值(只会走一次),顺序是:_key、_isKey、key、isKey。
    1. 定义一个收集实例变量的可变数组,通过class_copyIvarList获取对象中ivar列表,通过ivar_getName,获取ivar名称
    2. 通过class_getInstanceVariable方法,获取相应的ivar
    3. 通过object_setIvar方法,对相应的ivar设置值
  6. 如果找不到相关实例变量,则抛出异常。

hq_setValue:forKey:代码如下:

- (void)hq_setValue:(id)value forKey:(NSString *)key{
    if (key == nil || key.length == 0) {
        return;
    }
    //将key值的首字符变成大写,为后续补充set做准备
    NSString* Key = [key capitalizedString];
    //拼接setKey方法名
    NSString* setKey = [[NSString alloc] initWithFormat:@"set%@:", Key];
    //拼接_setKey方法名
    NSString* _setKey = [[NSString alloc] initWithFormat:@"_set%@:", Key];
    //拼接setIsKey方法名
    NSString* setIsKey = [[NSString alloc] initWithFormat:@"setIs%@:", Key];
    
    //依次执行上面的三个方法
    BOOL result = [self hq_performSelectorWithMethodName:setKey value:value];
    if(result){
        return ;
    }
    result = [self hq_performSelectorWithMethodName:_setKey value:value];
    if(result){
        return ;
    }
    result = [self hq_performSelectorWithMethodName:setIsKey value:value];
    if(result){
        return ;
    }
    
    //若未找到setter方法,则需要判断 accessInstanceVariablesDirectly 这个值
    result = [self.class accessInstanceVariablesDirectly];
    if(!result){
        @throw [NSException exceptionWithName:@"HQUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    //开始查找成员变量
    NSArray* ivalArr = [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([ivalArr containsObject:_key]){
        Ivar ivar = class_getInstanceVariable(self.class, _key.UTF8String);
        object_setIvar(self, ivar, value);
        return ;
    }
    else if([ivalArr containsObject:_isKey]){
        Ivar ivar = class_getInstanceVariable(self.class, _isKey.UTF8String);
        object_setIvar(self, ivar, value);
        return ;
    }
    else if([ivalArr containsObject:key]){
        Ivar ivar = class_getInstanceVariable(self.class, key.UTF8String);
        object_setIvar(self, ivar, value);
        return ;
    }
    else if([ivalArr containsObject:isKey]){
        Ivar ivar = class_getInstanceVariable(self.class, isKey.UTF8String);
        object_setIvar(self, ivar, value);
        return ;
    }
    
    //如果找不到相关实例,则抛出异常
    @throw [NSException exceptionWithName:@"HQUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

自定义 KVC 取值

  1. 异常判断,当key为空时,则不继续操作。

  2. 查找是否存在getter方法,按照查找顺序定义getter方法的方法名称,查找顺序:get<Key> --> <Key> --> is<Key> --> _<key>

  3. getter方法未找到,此时针对集合类型做了一些特殊处理。查找是否存在countOf<Key>方法
    3.1 若存在countOf<Key>方法
    3.1.1 查找是否存在objectIn<Key>AtIndex:或者<Key>AtIndexes:
    3.1.1.1 若存在,则执行objectIn<Key>AtIndex:<Key>AtIndexes:
    3.1.1.2 若不存在,则进入3.1.2
    3.1.2 查找是否存在enumeratorOf<Key>:并且存在memberOf<Key>:
    3.1.2.1 若存在,则执行enumeratorOf<Key>:memberOf<Key>:
    3.1.2.2 若不存在,则进入步骤4
    3.2 若不存在countOf<Key>方法,则进入步骤4

  4. 检查accessInstanceVariablesDirectly这个布尔值
    若为YES,则查找成员变量,查找流程:_<Key> --> _is<Key> --> <Key> --> is<Key>
    若为NO,则直接进入步骤5

  5. 若是以上都查找失败,则执行 valueForUndefinedKey: 方法,应用可重写该方法

    hq_setValue:forKey:代码如下:

- (id)hq_valueForKey:(NSString *)key{
    if (key == nil  || key.length == 0) {
        return nil;
    }
    //查找getter方法 get<Key> --> <key> --> is<Key> --> _<key>
    NSString* Key = [key capitalizedString];
    NSString* getKey = [NSString stringWithFormat:@"get%@", Key];
    NSString* isKey = [NSString stringWithFormat:@"is%@", Key];
    NSString* _key = [NSString stringWithFormat:@"_%@", key];
    
    id value = [self hq_performSelectorWithMethodName:getKey];
    if(value){
        return value;
    }
    value = [self hq_performSelectorWithMethodName:key];
    if (value) {
        return value;
    }
    value = [self hq_performSelectorWithMethodName:isKey];
    if (value) {
        return value;
    }
    value = [self hq_performSelectorWithMethodName:_key];
    if (value) {
        return value;
    }
    
    //查找countOfKey:方法 + objectIn<Key>AtIndex: 或者 <Key>AtIndexes:
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
    NSString *keyAtIndexes = [NSString stringWithFormat:@"%@AtIndexes:", key];
    NSString *enumeratorOfKey = [NSString stringWithFormat:@"enumeratorOf%@",Key];
    NSString *memberOfKey = [NSString stringWithFormat:@"memberOf%@:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    BOOL result = [self respondsToSelector:NSSelectorFromString(countOfKey)];
    if(result){
        [self performSelector:NSSelectorFromString(countOfKey)];
        if([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]){//countOfKey + objectInKeyAtIndex
            int count = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray* retArr = [[NSMutableArray alloc] init];
            for (int index = 0; index<count; index++) {
                IMP imp = class_getMethodImplementation(self.class, NSSelectorFromString(objectInKeyAtIndex));
                id (*func)(id, SEL, int) = (void*)imp;
                id obj = func(self, NSSelectorFromString(objectInKeyAtIndex),index);
                [retArr addObject:obj];
            }
            return retArr;
        }
        if([self respondsToSelector:NSSelectorFromString(keyAtIndexes)]){//countOfKey + keyAtIndexes
            int count = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray* retArr = [[NSMutableArray alloc] init];
            for (int index = 0; index<count; index++) {
                NSIndexSet* indexes = [[NSIndexSet alloc] initWithIndex:index];
                id obj = [self performSelector:NSSelectorFromString(keyAtIndexes) withObject:indexes];
                [retArr addObjectsFromArray:obj];
            }
            return retArr;
        }
        if ([self respondsToSelector:NSSelectorFromString(enumeratorOfKey)] && [self respondsToSelector:NSSelectorFromString(memberOfKey)]) {
            [self performSelector:NSSelectorFromString(countOfKey)];
            id enumerator = [self hq_performSelectorWithMethodName:enumeratorOfKey];
            if(enumerator){
                return [[NSSet alloc] initWithArray:[enumerator allObjects]];
            }
        }
    }
#pragma clang diagnostic pop
    
    //若未找到setter方法,则需要判断 accessInstanceVariablesDirectly 这个值
    result = [self.class accessInstanceVariablesDirectly];
    if(!result){
        @throw [NSException exceptionWithName:@"HQUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    //开始查找成员变量
    NSArray* ivalArr = [self getIvarListName];
    NSLog(@"ivalArr:%@", ivalArr);
    
    // _<Key> --> _is<Key> --> <Key> --> is<Key>
    NSString* _isKey = [NSString stringWithFormat:@"_is%@", Key];
    if([ivalArr containsObject:_key]){
        Ivar ivar = class_getInstanceVariable(self.class, _key.UTF8String);
        return object_getIvar(self, ivar);
    }
    else if([ivalArr containsObject:_isKey]){
        Ivar ivar = class_getInstanceVariable(self.class, _isKey.UTF8String);
        return object_getIvar(self, ivar);
    }
    else if([ivalArr containsObject:key]){
        Ivar ivar = class_getInstanceVariable(self.class, key.UTF8String);
        return object_getIvar(self, ivar);
    }
    else if ([ivalArr containsObject:isKey]){
        Ivar ivar = class_getInstanceVariable(self.class, isKey.UTF8String);
        return object_getIvar(self, ivar);
    }
    
    @throw [NSException exceptionWithName:@"HQUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    return @"";
}

KVC 运算符

  • @avg
    当指定@avg运算符时,valueForKeyPath:将会读取集合中每个元素,并将其转换为double(用0代替nil值),计算这些值的算术平均值。然后,返回存储在NSNumber对象中的结果。

  • @count
    指定@count运算符时,valueForKeyPath:返回集合对象中的对象数

  • @max
    当指定@max操作符时,valueForKeyPath:将搜索集合中各项,并返回最大的那个值。

  • @min
    当指定@min操作符时,valueForKeyPath:将搜索集合中各项,并返回最小的那个值。

  • @sum
    当指定@sum运算符时,valueForKeyPath:将会读取集合中每个元素,并将其转换为double(用0代替nil值),计算这些值的总和。然后,返回存储在NSNumber对象中的结果。

  • @distinctUnionOfObjects
    当您指定@distinctUnionOfObjects运算符时,valueForKeyPath:创建并返回一个数组,该数组包含与右键路径指定的属性相对应的集合的不同对象。并且会省略重复值

  • @unionOfObjects
    当您指定@unionOfObjects运算符时,valueForKeyPath:创建并返回一个数组,其中包含与右键路径指定的属性相对应的集合的所有对象。与@distinctUnionOfObjects不同,不会删除重复的对象

  • @distinctUnionOfArrays
    当您指定@distinctUnionOfArrays运算符时,valueForKeyPath:创建并返回一个数组,该数组包含与右键路径指定的属性相对应的所有集合的组合不同对象。并且会省略重复项

  • @unionOfArrays
    当您指定@unionOfArrays运算符时,将valueForKeyPath:创建并返回一个数组,该数组包含与由右键路径指定的属性相对应的所有集合的组合所有对象,而不会删除重复项

  • @distinctUnionOfSets
    当您指定@distinctUnionOfSets运算符时,将valueForKeyPath:创建并返回一个NSSet对象,该对象包含与右键路径指定的属性相对应的所有集合的组合中的不同对象。该运算符的行为类似于@distinctUnionOfArrays,只是它期望NSSet包含NSSet对象实例而不是NSArray对象实例。

    以上运算符如何使用请查看文末Demo。

KVC异常处理

  • 找不到key
    在设值/取值时,经过了KVC的一系列步骤之后,仍然找不到key,此时就会抛出异常。对于这种情况,可以重写以下方法:
    设值:setValue:forUndefinedKey:
    取值:valueForUndefinedKey:
    在上面两个方法重写一些逻辑,使得找不到key程序不会发生崩溃,但是一般不太建议这样。

  • 当值为nil时
    setValue:forKey:时,当value为nil时,程序会根据当前对象是否支持nil而抛出异常,若对象支持nil,则不会发生崩溃但是错误很隐蔽,不容易被发现。若对象不支持nil,则会发生崩溃。
    对于这种情况,当value为nil时,调用setValue:forKey:时,KVC会调用setNilValueForKey:方法,因此可以在应用中重写该方法,从而避免程序崩溃问题。

KVC应用场景

  • 动态的设值和取值
    • 通过setValue:forKey:valueForKey:方法进行设值、取值。
    • 通过setValue:forKeyPath:valueForKeyPath:方法进行设值、取值。
  • 通过KVC访问和修改私有变量
    在日常开发中,对于类的私有属性,在外部定义的对象,是无法直接访问私有属性的,但是对于KVC而言,一个对象没有自己的隐私,所以可以通过KVC修改和访问任何私有属性
  • 多值操作
    model和字典的转换可以通过下面两个KVC的API实现
    //字典转模型
    - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    
    //模型转字典
    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    
  • 修改一些系统空间的内部属性
    在日常开发中,我们知道,很多UI控件都是在其内部由多个UI空间组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,常用的就是自定义tabbar个性化UITextField中placeHolderText
  • 用KVC实现高阶消息传递
    在对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合来返回另一个集合。

Demo

自定义KVC见Demo源码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容