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);// }@endint 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

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

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