底层原理-18-KVC探索|8月更文挑战

336 阅读9分钟

1.kvc简介

kvc是我们日常开发常用的一套api,全称是Key-Value Coding。源码中没有开发,我们可以去官方文档看下怎么实现的

image.png 键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该机制提供对其属性的间接访问。当对象符合键值编码要求时,其属性可以通过字符串参数通过简洁、统一的消息接口寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。

通常使用访问器方法来访问对象的属性。获取访问器(或获取器)返回属性的值集合访问器(或设置器)设置属性的值。在Objective-C中,您还可以直接访问属性的基础实例变量。以任何一种方式访问对象属性很简单,但需要调用特定于属性的方法或变量名。随着属性列表的增加或更改,访问这些属性的代码也必须增加或更改。相比之下,键值编码兼容对象提供了一个简单的消息接口,该接口在其所有属性上都是一致的。

键值编码是一个基本概念,是许多其他cocoa技术的基础,例如键值观察、cocoa绑定、核心数据和AppleScript能力。在某些情况下,键值编码也有助于简化代码

为了使您自己的对象键值编码兼容,您确保它们采用NSKeyValueCoding非正式协议并实现相应的方法,例如valueForKey:作为泛型获取器和setValue:forKey:作为泛型设置器。幸运的是,如上所述,NSObject采用了此协议,并为这些和其他基本方法提供了默认实现。因此,如果您从NSObject(或其多个子类中的任何一个派生对象),大部分工作已经为您完成。

2.常用API

2.1 访问属性

  • setValue:forKey:valueForKey:

    对象根据key设置value和对象根据value取值。

  • setValue:forKeyPath:valueForKeyPath:

    通过keyPath (即路由)设值/取值

  • setValue:forUndefinedKey: 如果指定的密钥对应于接收setter调用的对象不具有的属性,则该对象会向自己发送消息。setValue:forUndefinedKey:的默认实现引发NSUndefinedKeyException。但是,子类可以重写此方法,以自定义方式处理请求。

  • setValuesForKeysWithDictionary: dictionaryWithValuesForKeys:

    对一组key和一组value进行赋值和取值,相当于对字典进行赋值和取值。

2.2 集合对象

  • mutableArrayValueForKey:mutableArrayValueForKeyPath:

    这些返回一个行为类似于NSMutableArray对象的代理对象。

  • mutableSetValueForKey:mutableSetValueForKeyPath:

    这些返回一个行为类似于NSMutableSet对象的代理对象。

  • mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:

    这些返回一个行为类似于NSMutableOrderedSet对象的代理对象。

2.3 其它

  • validateValue: forKey: error:

    由于特定于属性的验证方法通过引用接收值和错误参数。

  • +(BOOL)accessInstanceVariablesDirectly;

默认返回YES,表示如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索

  • -(void)setNilValueForKey:(NSString *)key ; 设置空值的时候调用

3. KVC API具体体现

3.1 非正式协议 - 间接访问

    LGPerson *person = [[LGPerson alloc] init];
    // 一般setter 方法
    person.name      = @"KB"; 
    // getter方法
    NSLog(@"%@ - %d - %@",person.name,person.age,person->myName);
    // kvc 赋值
    [person setValue:@"KB" forKey:@"name"];
    //kvc 取值
    NSLog(@"%@",[person valueForKey:@"name"]);

一般对于属性我们使用setter和getter进行赋值和取值,对应的setValue和valueForKey。

3.2 KVC - 集合类型

person.array = @[@"1",@"2",@"3"];

    // 修改数组

    // person.array[0] = @"100";

    // 第一种:搞一个新的数组 - KVC 赋值就OK

    NSArray *array = [person valueForKey:@"array"];

    array = @[@"100",@"2",@"3"];

    [person setValue:array forKey:@"array"];

    NSLog(@"%@",[person valueForKey:@"array"]);

    // 第二种通过mutableArrayValueForKey取出值,进行改变

    NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];

    mArray[0] = @"200";

    NSLog(@"%@",[person valueForKey:@"array"]);

确定数组类型可以mutableArrayValueForKey根据key返回一个可变数组,对数组进行操作。

3.3 KVC - 集合操作符

// @avg、@count、@max、@min、@sum

- (void)aggregationOperator{

    NSMutableArray *personArray = [NSMutableArray array];

    for (int i = 0; i < 6; i++){

        LGStudent *p = [LGStudent new];

        NSDictionary* dict = @{

                               @"name":@"Tom",

                               @"age":@(18+i),

                               @"nick":@"Cat",

                               @"length":@(175 + 2*arc4random_uniform(6)),

                               };

        [p setValuesForKeysWithDictionary:dict];

        [personArray addObject:p];

    }

    NSLog(@"%@", [personArray valueForKey:@"length"]);

    

    /// 平均身高

    float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];

    NSLog(@"%f", avg);

    

    int count = [[personArray valueForKeyPath:@"@count.length"] intValue];

    NSLog(@"%d", count);

    

    int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];

    NSLog(@"%d", sum);

    

    int max = [[personArray valueForKeyPath:@"@max.length"] intValue];

    NSLog(@"%d", max);

    

    int min = [[personArray valueForKeyPath:@"@min.length"] intValue];

    NSLog(@"%d", min);

}

image.png 集合操作符以某种方式聚合集合的对象,并返回一个通常与右键路径中命名的属性的数据类型匹配的单个对象。聚合运算符对数组一组属性进行工作,生成反映集合某些方面的单个值

3.4 KVC - 阵列运算符

// 数组操作符 @distinctUnionOfObjects @unionOfObjects

- (void)arrayOperator{

    NSMutableArray *personArray = [NSMutableArray array];

    for (int i = 0; i < 6; i++) {

        LGStudent *p = [LGStudent new];

        NSDictionary* dict = @{

                               @"name":@"Tom",

                               @"age":@(18+i),

                               @"nick":@"Cat",

                               @"length":@(175 + 2*arc4random_uniform(6)),

                               };

        [p setValuesForKeysWithDictionary:dict];

        [personArray addObject:p];

    }

    NSLog(@"%@", [personArray valueForKey:@"length"]);

    // 返回操作对象指定属性的集合

    NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.length"];

    NSLog(@"arr1 = %@", arr1);

    // 返回操作对象指定属性的集合 -- 去重

    NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];

    NSLog(@"arr2 = %@", arr2);
}

通过distinctUnionOfObjects(去重)和unionOfObjects指定某个key返回一个集合

image.png

3.4 KVC- 嵌套运算符

// 嵌套集合(array&set)操作 @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets

- (void)arrayNesting{

    

    NSMutableArray *personArray1 = [NSMutableArray array];

    for (int i = 0; i < 6; i++) {

        LGStudent *student = [LGStudent new];

        NSDictionary* dict = @{

                               @"name":@"Tom",

                               @"age":@(18+i),

                               @"nick":@"Cat",

                               @"length":@(175 + 2*arc4random_uniform(6)),

                               };

        [student setValuesForKeysWithDictionary:dict];

        [personArray1 addObject:student];

    }

    

    NSMutableArray *personArray2 = [NSMutableArray array];

    for (int i = 0; i < 6; i++) {

        LGPerson *person = [LGPerson new];

        NSDictionary* dict = @{

                               @"name":@"Tom",

                               @"age":@(18+i),

                               @"nick":@"Cat",

                               @"length":@(175 + 2*arc4random_uniform(6)),

                               };

        [person setValuesForKeysWithDictionary:dict];

        [personArray2 addObject:person];

    }

    

    // 嵌套数组

    NSArray* nestArr = @[personArray1, personArray2];

    

    NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];

    NSLog(@"arr = %@", arr);//去重

    

    NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];

    NSLog(@"arr1 = %@", arr1);//不去重

}

image.png 嵌套运算符对嵌套集合进行操作,其中集合本身的每个条目都包含一个集合。

3.5 KVC- 结构体属性

typedef struct {

    float x, y, z;

} ThreeFloats;
@property (nonatomic)         ThreeFloats       threeFloats;
ThreeFloats floats = {1.,2.,3.};

    NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];

    [person setValue:value forKey:@"threeFloats"];

    NSValue *value1    = [person valueForKey:@"threeFloats"];

    NSLog(@"%@",value1);
    ThreeFloats th;

    [value1 getValue:&th];

    NSLog(@"%f-%f-%f",th.x,th.y,th.z);

image.png 默认实现用getValue:消息解包值,然后使用结果结构调用setThreeFloats:

3.6 KVC - KeyPath

@property (nonatomic, strong) LGStudent         *student;
LGStudent *student = [LGStudent alloc];

    student.subject    = @"hello";

    person.student     = student;

    [person setValue:@"Swift" forKeyPath:@"student.subject"];

    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);

使用路由进行访问,找到依赖的关系,进而访问关联对象的属性。

4. KVC 赋值&取值原理

4.1 赋值

setValue:forKey:Foundation框架中,但是不开源。我们去官方文档看看
我们下面以流程以设置LGPerson的对象person的属性name为例:

1.按此顺序查找第一个名为set<Key>:_set<Key>setIs<key>的访问器。如果找到,请使用输入值(或根据需要打开的值)调用它,然后完成

image.png set<Key>:_set<Key>同时存在走set<Key>:

image.png 不存在set<Key>:_set<Key>

image.png set<Key>:_set<Key>都不存在去查找setIs<key>

  1. 如果没有找到简单的访问器,并且类方法accessInstanceVariablesDirectly返回YES,请按此顺序查找名称为_<key>_is<Key><key>is<Key>的实例变量。如果找到,直接使用输入值(或解包装值)设置变量并完成。
//定义成员变量
@interface LGPerson : NSObject{

    @public

    NSString *_isName;

    NSString *name;

    NSString *isName;

    NSString *_name;

}
#pragma mark - 关闭或开启实例变量赋值

+ (BOOL)accessInstanceVariablesDirectly{

    return YES;//默认yes

}
//打印
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);

正常情况下对name赋值没有问题。我们注释name,进行打印

image.png

[person setValue:@"LG_Cooci" forKey:@"name"];

image.pngname进行赋值但是却赋值到了_name的实例变量。

image.png image.pngname进行赋值但是却赋值到了_isName的实例变量。
3. 在找不到访问器或实例变量时,调用setValue:forUndefinedKey:。默认情况下,这将引发异常,但NSObject的子类可能会提供特定于密钥的行为。

4.2 取值

  1. 按此顺序,在实例中搜索第一个以get<Key><key>is<Key>_<key>等名称找到的第一个访问器方法。如果找到,请调用它,然后继续第5步,并得出结果。否则继续下一步。

image.png按顺序查找实现。

2.如果找到其中第一个和其他两个中的至少一个,创建一个响应所有NSArray方法的集合代理对象并返回该对象。否则,请继续第3步。
3. 如果找到所有三种方法,请创建一个响应所有NSSet方法的集合代理对象并返回该对象。否则,请继续第4步。
4. 如果还没有找到,检查类方法InstanceVariablesDirectly是否YES,依次搜索_<key>,_is<Key>,<key>或is<Key>的实例变量
5. 如果检索到的属性值是对象指针,只需返回结果。如果该值是NSNumber支持的标量类型,请将其存储在NSNumber实例中并返回该值。 如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。

image.png 注释_name的赋值

image.png 只有注释了_name的变量才不会去获取,去获取_isName
结论: 依次搜索_<key>,_is<Key>,<key>或is<Key>的实例变量

image.png 6. 如果其他方法都失败,请调用 valueForUndefinedKey:。默认情况下,这将引发异常,但NSObject的子类可能会提供特定于密钥的行为。

5. 自定义KVC

在NSObject的分类中我们自定义KVC

@interface NSObject (KBKVC)

//设值

-(void)kb_setValue:(nullable id)value forKey:(NSString*)key;

//取值

-(nullable id)kb_valueForKey:(NSString*)key;

@end

5.1 赋值

1.判断key是否存在
2.实现setter方法,set<key>,_set<key>,setIs<key> 依次判断是否实现
3.都没有实现手动开关,默认是yes。判断是否响应 accessInstanceVariablesDirectly 返回YES, NO 奔溃
4. 获取成员变量ivar名字 -> 遍历 containsObjct -,请按此顺序查找名称为_<key>_is<Key><key>is<Key>实例变量
4.1 获取对应的成员变量
4.2 设置对应成员变量的值
5.找不到相关实例报错
具体代码:

-(void)kb_setValue:(id)value forKey:(NSString *)key

{

    //1.判断key是否存在

    

    if (key == nil || key.length == 0) {

        return;

    }

    //2.实现setter方法,set<key>,_set<key>,setIs<key>

    NSString *Key = key.capitalizedString;//首字母大写

    //拼接方法

    NSString *setKey = [NSString stringWithFormat:@"set%@",Key];

    NSString *_setKey = [NSString stringWithFormat:@"_set%@",Key];

    NSString *isSetKey = [NSString stringWithFormat:@"setIs%@",Key];

    

    //依次判断是否实现

    if ([self kb_performSelectorWithMethodName:setKey value:value]) {

        NSLog(@"*********%@**********",setKey);

        return;

    }else if ([self kb_performSelectorWithMethodName:_setKey value:value])

    {

        NSLog(@"*********%@**********",_setKey);

        return;

    }else if([self kb_performSelectorWithMethodName:isSetKey value:value])

    {

        NSLog(@"*********%@**********",isSetKey);

        return;

    }

    //3. 都没有实现手动开关,默认是yes

    //判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃

    if (![self.class accessInstanceVariablesDirectly]) {

        @throw [NSException exceptionWithName:@"KBUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];

    }

    

    //4: 间接变量

    //获取 成员变量ivar名字 -> 遍历 containsObjct -

    NSMutableArray *mArr = [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 ([mArr containsObject:_key]) {

        //4.1 获取对应的成员变量

        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);

        //4.2 设置对应成员变量的值

        object_setIvar(self, ivar, value);

        return;

    }else if([mArr containsObject:_isKey]){

        

        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);

        object_setIvar(self, ivar, value);

        return;

    }else if([mArr containsObject:key]){

        

        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);

        object_setIvar(self, ivar, value);

        return;

    }else if([mArr containsObject:isKey]){

        

        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);

        object_setIvar(self, ivar, value);

        return;

    }

    

    //5.找不到相关实例报错

    @throw [NSException exceptionWithName:@"KBUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];

    

}
#pragma mark - 相关方法

- (BOOL)kb_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;

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

}

5.2 取值

  1. 判断key是否存在
  2. get<Key><key>is<Key>_<key>方法按顺序查找是否实现,没有实现进行下一步
  3. 判断手动设置accessInstanceVariablesDirectly是否为YES,默认是yes,设置NO,报错。yes的话进行下一步
  4. 间接变量 获取成员变量ivar名字 -> 遍历containsObjct- 依次搜索_<key>_is<Key><key>is<Key>的实例变量
-(nullable id)kb_valueForKey:(NSString *)key

{

    //1.判断key是否存在

    

    if (key == nil || key.length == 0) {

        return nil;

    }

    //2.实现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];

    //依次判断是否实现

    if([self respondsToSelector:NSSelectorFromString(getKey)])

    {

        return [self kb_performSelectorWithMethodName:getKey];

    }else if ([self respondsToSelector:NSSelectorFromString(key)]) {

        

        return [self kb_performSelectorWithMethodName:key];

    }else if ([self respondsToSelector:NSSelectorFromString(isKey)])

    {

        return [self kb_performSelectorWithMethodName:isKey];

    }else if([self respondsToSelector:NSSelectorFromString(_key)])

    {

        return [self kb_performSelectorWithMethodName:_key];

    }

    

    // 3:判断是否能够直接赋值实例变量

    if (![self.class accessInstanceVariablesDirectly] ) {

        @throw [NSException exceptionWithName:@"KBUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];

    }

    

    //4:

    //间接变量 获取成员变量ivar名字 -> 遍历 containsObjct -

    NSMutableArray *mArr = [self getIvarListName];

    //依次搜索_<key>,_is<Key>,<key>或is<Key>的实例变量

    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];

    

    if ([mArr containsObject:_key]) {

        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);

        return  object_getIvar(self, ivar);

    }else if([mArr containsObject:_isKey])

    {

        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);

        return  object_getIvar(self, ivar);

    }else if([mArr containsObject:key]){

        

        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);

        return object_getIvar(self, ivar);

    }else if([mArr containsObject:isKey]){

        

        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);

        return object_getIvar(self, ivar);

        

    }

    

    

    return @"";

    

}
- (id)kb_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;

}

6. 总结

kvc键值编码,是一种非正式协议,这种机制可以帮助我们访问一些私有成员变量,也可以修改系统的一些属性。可以动态的赋值和取值,也可以方便我们某些操作,如取对象数组某一个属性的最大值最小值等。
我们在探索过程中,发现kvc给了很多容错机制,防止我们误写导致无法获取值和取值。
大概流程:

未命名文件-5.jpg