关于ios:iOS底层学习KVC

44次阅读

共计 14345 个字符,预计需要花费 36 分钟才能阅读完成。

1.KVC 协定定义

键值编码是由 NSKeyValueCoding 非正式协定 启用的一种机制,对象采纳该机制来提供 对其属性的间接拜访 。当对象合乎键值编码时,其属性可通过字符串参数通过简洁、对立的 消息传递接口 进行寻址。这种间接拜访机制补充了实例变量及其相干拜访器办法提供的间接拜访。

本文收录:掘金【gufs 镜像】《iOS 底层学习——KVC》

  • KVC 在 Objective- C 中的定义

    KVC的定义都是对 NSObject 的扩大来实现的 (Objective-C 中有个显式的 NSKeyValueCoding 类别名 -分类 )。查看setValueForKey 办法,发现其在 Foundation 外面,而 Foundation 框架是 不开源 的,只能在苹果官网文档查找。见下图:

2.KVC 提供的 API 办法

  • 咱们能够学习解读苹果的官网文档,对 KVC 有更深的了解。

    Key-Value Coding Programming Guide

    苹果对一些容器类比方 NSArray 或者 NSSet 等,KVC有着非凡的实现。

  • 罕用办法

    对于所有继承了 NSObject 的类型,也就是简直所有的 Objective-C 对象都能应用 KVC,上面是KVC 最为重要的四个办法:

       - (nullable id)valueForKey:(NSString *)key;                          // 间接通过 Key 来取值
       - (void)setValue:(nullable id)value forKey:(NSString *)key;          // 通过 Key 来设值
       - (nullable id)valueForKeyPath:(NSString *)keyPath;                  // 通过 KeyPath 来取值
       - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  // 通过 KeyPath 来设值
    
  • 非凡办法

    当然 NSKeyValueCoding 类别中还有其余的一些办法,这些办法在碰到非凡状况或者有非凡需要还是会用到的。

    // 默认返回 YES,示意如果没有找到 Set 办法的话,会依照_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在进行构造体解决时,须要用到NSValue,设值时,将构造体封装成NSValue,进行键值设值;取值同样返回NSValue,而后依照构造体格式进行解析,见上面代码:

        // 构造体
        ThreeFloats floats = {1.,2.,3.};
        // 封装成 NSValue
        NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
        // 设值
        [person setValue:value forKey:@"threeFloats"];
    
        // 取值
        NSValue *value1    = [person valueForKey:@"threeFloats"];
        // 构造体解析
        ThreeFloats th;
        [value1 getValue:&th];
        NSLog(@"%f-%f-%f",th.x,th.y,th.z);
    
  • 字典解决(模型转换)

    字典能够实现与模型进行装换,也能够通过键值数组从模型中获取字典数据。见上面代码:

    - (void)dictionaryTest{
        // 字典
        NSDictionary* dict = @{
                               @"name":@"Cooci",
                               @"nick":@"KC",
                               @"subject":@"iOS",
                               @"age":@18,
                               @"length":@180
                               };
        // 模型
        LGStudent *p = [[LGStudent alloc] init];
        // 字典转模型
        [p setValuesForKeysWithDictionary:dict];
    
        // 键值数组
        NSArray *array = @[@"name",@"age"];
        // 从模型中获取响应的字典数据
        NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
        NSLog(@"%@",dic);
    }
    

3.KVC 设值取值程序

KVC是怎么应用的,咱们都很分明,那么 KVC 在外部是按什么样的程序来寻找 key 的呢?这是咱们要摸索的重点。

1. 设值

当调用 setValue:forKey: 代码时,底层的执行机制是怎么的呢?在官网文档中有相干的阐明,见下图:

  • 翻译过去的意思是:

    setValue:forKey: 的默认实现,给定 keyvalue参数作为输出,尝试将名为 key 的属性设置为 value,在接管调用的对象外部,应用以下过程:按程序查找名为 set<Key>:_set<Key> 的第一个拜访器。如果找到,则应用输出值(或依据须要开展的值)调用它并实现。如果未找到简略拜访器,并且类办法 accessInstanceVariablesDirectly 返回 YES,则按程序查找名称相似于 _<key>_is<Key><key>is<Key> 的实例变量。如果找到,间接应用输出值(或解包值)设置变量并实现。在未找到拜访器或实例变量时,调用 setValue:forUndefinedKey:。默认状况下,这会引发异样,但 NSObject 的子类可能会提供特定于键的行为。

  • 依据上的官网内容,能够得出如下实现机制:

    • 按程序查找名为 set<Key>_set<Key> 或者setIs<Key>setter拜访器程序查找,如果找到就调用它
    • 只有实现任意一个,那么就会将调用这个办法,将属性的值设为传进来的值
    • 如果没有找到这些 setter 办法,KVC机制会查看 + (BOOL)accessInstanceVariablesDirectly 办法有没有返回 YES,默认该办法会返回YES,如果重写了该办法让其返回NO 的话,那么在这一步 KVC 会执行 setValue:forUndefinedKey: 办法;
    • 如果返回 YESKVC 机制会优先搜寻该类外面有没有名为 _<Key> 的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的拜访修饰符,只在存在以 _<Key> 命名的变量,KVC都能够对该成员变量赋值
    • KVC机制再会持续搜寻 _is<Key><key>is<key>的成员变量,再给它们赋值
    • 如果下面列出的办法或者成员变量都不存在,零碎将会执行该对象的 setValue:forUndefinedKey: 办法,默认是抛出异样。
  • [person setValue:@"newName" forKey:@"name"]; 为例,能够得出以下论断:

    • 优先通过 setter 办法,进行属性设置,调用程序是:

      1. setName
      2. _setName
      3. setIsName
    • 如果以上办法均未找到,并且 accessInstanceVariablesDirectly 返回YES,则通过成员变量进行设置,程序是:

      1. _name
      2. _isName
      3. name
      4. isName

    可通过案例进行验证,这里不再展现。

  • accessInstanceVariablesDirectly阐明

    重写 + (BOOL)accessInstanceVariablesDirectly 办法让其返回 NO,这样的话,如果KVC 没有找到 set<Key>_set<Key>setIs<Key> 相干办法时,会间接用 setValue:forUndefinedKey: 办法。咱们用代码来测试一下下面的 KVC 机制:

    @interface LGPerson : NSObject
    {
        @public
            NSString *_isName;
            NSString *name;
            NSString *isName;
            NSString *_name;
    }
    @end
    @implementation LGPerson
    
    +(BOOL)accessInstanceVariablesDirectly{return NO;}
    
    -(id)valueForUndefinedKey:(NSString *)key{NSLog(@"出现异常,该 key 不存在 %@",key);
        return nil;
    }
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{NSLog(@"出现异常,该 key 不存在 %@",key);
    }
    
    // 设置办法全副正文掉
    // -(void)setName:(NSString*)name{
    //     toSetName = name;
    // }
    // - (void)_setName:(NSString *)name{//     NSLog(@"%s - %@",__func__,name);
    // }
    // - (void)setIsName:(NSString *)name{//     NSLog(@"%s - %@",__func__,name);
    // }
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            LGPerson* person = [LGPerson new];
            [person setValue:@"NewName" forKey:@"name"];
            NSString* name = [person valueForKey:@"name"];
            NSLog(@"value for key : %@",name);
    
            NSLog(@"取值_name:%@",person->_name);
            NSLog(@"取值_isName:%@",person->_isName);
            NSLog(@"取值 name:%@",person->name);
            NSLog(@"取值 isName:%@",person->isName);
        }
        return 0;
    }
    

    运行构造见下图:

    这阐明了重写 +(BOOL)accessInstanceVariablesDirectly 办法让其返回 NO 后,KVC找不到 set<Key> 等办法后,不再去找 <Key> 系列成员变量,而是间接调用 setValue:forUndefinedKey: 办法,所以开发者如果不想让本人的类实现KVC,就能够这么做。

  • KVC 设值流程图

2. 取值

当调用 valueForKey: 的代码时,底层的执行机制又是怎么的呢?在官网文档中有相干的阐明,见下图:

  • 依据上的官网内容,翻译之后能够得出如下实现机制:

    • 首先按 get<Key><Key>is<Key>_<Key> 的程序办法查找 getter 办法,找到的话会间接调用,如果是 BOOL 或者 Int 等值类型,会将其包装成一个 NSNumber 对象。
    • 如果下面的 getter 没有找到,KVC则会查找 countOf<Key>objectIn<Key>AtIndex<Key>AtIndexes格局的办法。如果 countOf<Key> 办法和另外两个办法中的一个被找到,那么就会返回一个能够响应 NSArray 所有办法的代理汇合 (它是NSKeyValueArray,是NSArray 的子类),调用这个代理汇合的办法,或者说给这个代理汇合发送属于 NSArray 的办法,就会以 countOf<Key>objectIn<Key>AtIndexAt<Key>Indexes这几个办法组合的模式调用。还有一个可选的 get<Key>:range: 办法。所以你想从新定义 KVC 的一些性能,你能够增加这些办法,须要留神的是你的办法名要合乎 KVC 的规范命名办法,包含办法签名。
    • 如果下面的办法没有找到,那么会同时查找 countOf<Key>enumeratorOf<Key>memberOf<Key> 格局的办法。如果这三个办法都找到,那么就返回一个能够响应 NSSet 所的办法的代理汇合,和下面一样,给这个代理汇合发 NSSet 的音讯,就会以 countOf<Key>enumeratorOf<Key>memberOf<Key> 组合的模式调用。
    • 如果还没有找到,再查看类办法 + (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<Key>_is<Key><Key>is<Key> 的顺序搜索成员变量名,这里不举荐这么做,因为这样间接拜访实例变量毁坏了封装性,使代码更软弱。如果重写了类办法 + (BOOL)accessInstanceVariablesDirectly 返回 NO 的话,那么会间接调用valueForUndefinedKey:
    • 还没有找到的话,调用valueForUndefinedKey:
  • [person valueForKey:@"name"]; 为例

    • getter办法的调用程序是:

      1. getName
      2. name
      3. isName
      4. _name
    • 如果以上办法没有找到,accessInstanceVariablesDirectly 返回 YES,则间接返回成员变量,获取程序仍然是:

      1. _name
      2. _isName
      3. name
      4. isName

    可通过案例进行验证,这里不再展现。

  • KVC 取值流程图

能够通过上面的代码对以上论断进行验证!

    @interface LGPerson : NSObject
    {
        @public
            NSString *_isName;
            NSString *name;
            NSString *isName;
            NSString *_name;
    }
    @end
    @implementation LGPerson

    +(BOOL)accessInstanceVariablesDirectly{return NO;}

    -(id)valueForUndefinedKey:(NSString *)key{NSLog(@"出现异常,该 key 不存在 %@",key);
        return nil;
    }
    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{NSLog(@"出现异常,该 key 不存在 %@",key);
    }

    // 设置办法全副正文掉
    // -(void)setName:(NSString*)name{
    //     toSetName = name;
    // }
    // - (void)_setName:(NSString *)name{//     NSLog(@"%s - %@",__func__,name);
    // }
    // - (void)setIsName:(NSString *)name{//     NSLog(@"%s - %@",__func__,name);
    // }

    // 取值办法
    //- (NSString *)getName{//    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)name{//    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)isName{//    return NSStringFromSelector(_cmd);
    //}
    //- (NSString *)_name{//    return NSStringFromSelector(_cmd);
    //}
    @end

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            LGPerson* person = [LGPerson new];
            [person setValue:@"NewName" forKey:@"name"];
            NSString* name = [person valueForKey:@"name"];
            NSLog(@"value for key : %@",name);

            NSLog(@"取值_name:%@",person->_name);
            NSLog(@"取值_isName:%@",person->_isName);
            NSLog(@"取值 name:%@",person->name);
            NSLog(@"取值 isName:%@",person->isName);
        }
        return 0;
    }

4. 在 KVC 中应用 keyPath

除了对以后对象的属性进行赋值外,还能够对其更深层的对象进行赋值。例如,对以后对象的 location 属性的 country 属性进行赋值。KVC进行多级拜访时,间接相似于属性调用一样用点语法进行拜访即可。

    [person setValue:@""forKeyPath:@"location.country"];

通过 keyPath 对数组进行取值时,并且数组中存储的对象类型都雷同,能够通过 valueForKeyPath: 办法指定取出数组中所有对象的某个字段。例如上面例子中,通过 valueForKeyPath: 将数组中所有对象的 name 属性值取出,并放入一个数组中返回。

    NSArray *names = [array valueForKeyPath:@"name"];

5. 异样解决

当依据 KVC 搜寻规定,没有搜寻到对应的 key 或者keyPath,则会调用对应的异样办法。异样办法的默认实现,在异样产生时会抛出一个异样,并且应用程序Crash。见下图:

咱们能够重写上面两个办法:

    -(id)valueForUndefinedKey:(NSString *)key{NSLog(@"出现异常,该 key 不存在 %@",key);
        return nil;
    }

    -(void)setValue:(id)value forUndefinedKey:(NSString *)key{NSLog(@"出现异常,该 key 不存在 %@",key);
    }

重写这两个办法之后,运行程序不再解体,见下图:

然而咱们能够依据业务须要,正当的解决 KVC 导致的异样。比方上面的解决形式:

- (void)setNilValueForKey:(NSString *)key {if ([key isEqualToString:@"name"]) {[self setValue:@"" forKey:@”age”];
    } else {[super setNilValueForKey:key];
    }
}

6. 自定义 KVC

依据苹果官网文档提供的设值、取值规定,咱们能够本人进行 KVC 的自定义实现。见上面实现代码:

// KVC 自定义
@implementation NSObject (LGKVC)

// 设置
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
    // 1: 判断什么 key
    if (key == nil || key.length == 0) {return;}

    // 2: setter set<Key>: or _set<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: 判断是否响应 accessInstanceVariablesDirectly 返回 YES NO 奔溃
    // 3: 判断是否可能间接赋值实例变量——NO
    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: 间接变量
    // 获取 ivar -> 遍历 containsObjct -
    // 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];
}

// 取值
- (nullable id)lg_valueForKey:(NSString *)key{

    // 1: 刷选 key 判断非空
    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: 判断是否可能间接赋值实例变量 -YES、NO
    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 @"";
}

#pragma mark **- 相干办法 **

- (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;
}

@end

文末举荐:iOS 热门文集 & 视频解析

① Swift

② iOS 底层技术

③ iOS 逆向防护

④ iOS 面试合集

⑤ 大厂面试题 + 底层技术 + 逆向安防 +Swift

喜爱的小伙伴记得点赞喔~

珍藏等于白嫖,点赞才是真情ღ(´・ᴗ・`)ღ

正文完
 0