KVC原理分析及应用

前言:

KVC又称键值编码(Key-Value-Coding) ,在iOS开发中是一个比较常见的技术点,相信一般开发人员都会使用KVC,其主要的两个方法无非就是设置值和取值,相信也有不少人写UI喜欢使用storyboardxib,他们也经常会使用KVC赋值,如图

在这里插入图片描述
今天我们就整体做一下总结

一.KVC机制

附:KVC的官方文档

1.符合使用KVC的对象

在这里插入图片描述

翻译总结:

  • 1.对象属性
  • 2.集合(容器)属性
  • 3.集合对象上调用集合运算符
  • 4.访问非对象属性
  • 5.通过键路径访问属性

2.常用方法

以下是一些常用方法演示,在Foundation框架的NSKeyValueCoding中可以去看

获取属性值

@interface NSObject(NSKeyValueCoding)  //分类

- (nullable id)valueForKey:(NSString *)key;
//返回由 key 参数命名的属性的值。
//如果根据访问器搜索模式中描述的规则无法找到由键命名的属性,则该对象会向自身发送一条valueForUndefinedKey:消息。的默认实现valueForUndefinedKey:引发NSUndefinedKeyException,但子类可能会覆盖此行为并更优雅地处理这种情况。

- (nullable id)valueForKeyPath:(NSString *)keyPath;
// 返回指定密钥路径相对于接收者的值。键路径序列中不符合特定键的键值编码的任何对象(即,默认实现valueForKey:无法找到访问器方法)都会接收valueForUndefinedKey:消息。

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//返回相对于接收器的键数组的值。该方法调用valueForKey:数组中的每个键。返回的NSDictionary包含数组中所有键的值。
笔记:集合对象,如NSArray,NSSet和NSDictionary,不能包含nil的值,可以用NSNull替代

设置属性值

- (void)setValue:(nullable id)value forKey:(NSString *)key;
//将指定键的值相对于接收消息的对象设置为给定值。setValue:forKey:自动解包NSNumber和NSValue表示标量和结构的对象的默认实现并将它们分配给属性。有关包装和展开语义的详细信息,请参阅表示非对象值。
//如果指定的键对应于接收 setter 调用的对象没有的属性,则该对象会向自身发送一条setValue:forUndefinedKey:消息。的默认实现setValue:forUndefinedKey:引发了一个NSUndefinedKeyException. 但是,子类可以覆盖此方法以自定义方式处理请求。

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
//在相对于接收器的指定键路径上设置给定值。键路径序列中不符合特定键的键值编码的任何对象都会收到一条setValue:forUndefinedKey:消息。

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
//使用指定字典中的值设置接收器的属性,使用字典键来标识属性。默认实现调用setValue:forKey:为每个键-值对,用nil为NSNull对象作为必需的。

关于keyPath

通过keyPath可以进行深层次赋值,例如对象属性的属性,举例说明

    LGPerson *person = [[LGPerson alloc] init];
    
    person.student = [[LGStudent alloc]init];
    
    [person setValue:@"YCX" forKeyPath:@"student.name"];
    
    NSLog(@"取值:%@",[person.student valueForKeyPath:@"name"]);

打印结果

2021-07-31 15:21:00.380946+0800 002-KVC取值&赋值过程[5799:231000] 取值:YCX

3.特殊对象处理

集合属性
如:NSArray或者NSSet等,KVC有着特殊的实现,以下为官方文档的翻译

//符合键值编码的对象以与公开其他属性相同的方式公开其对多属性。您可以像使用valueForKey:and setValue:forKey:(或它们的键路径等价物)的任何其他对象一样获取或设置集合对象。但是,当您想要操作这些集合的内容时,使用协议定义的可变代理方法通常是最有效的。

//该协议为集合对象访问定义了三种不同的代理方法,每种方法都有一个键和一个键路径变体:

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
//它们返回一个行为类似于NSMutableArray对象的代理对象。

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
//它们返回一个行为类似于NSMutableSet对象的代理对象。

- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;
//它们返回一个行为类似于NSMutableOrderedSet对象的代理对象。

4.设值顺序

截取了部分设值相关的官方文档
在这里插入图片描述

大致意思是:

1.搜索名称类似于insertObject:in<Key>AtIndex:and removeObjectFrom<Key>AtIndex:(对应于NSMutableOrderedSet类定义的两个最原始方法)以及insert<Key>:atIndexes:and remove<Key>AtIndexes:(对应于insertObjects:atIndexes:and removeObjectsAtIndexes:)的方法。

如果发现至少一个插入方法和至少一种去除方法,返回的代理对象发送的一些组合insertObject:in<Key>AtIndex:,removeObjectFrom<Key>AtIndex:,insert<Key>:atIndexes:,和remove<Key>AtIndexes:消息发送到的原始接收器mutableOrderedSetValueForKey:当它接收到消息NSMutableOrderedSet的消息。

代理对象还使用名称类似于replaceObjectIn<Key>AtIndex:withObject:或replace<Key>AtIndexes:with<Key>:当它们存在于原始对象中的方法。

2.如果未找到可变集合方法,请搜索名称类似于set<Key>:. 在这种情况下,返回的代理对象每次收到消息时都会set<Key>:向原始接收者发送消息。 mutableOrderedSetValueForKey:NSMutableOrderedSet

这里我们主要得出的验证是设置顺序,以下代码为例

    LGPerson *person = [[LGPerson alloc] init];
    
    [person setValue:@"YCX" forKey:@"name"];

    NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);

//LGPerson.m
#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

通过不断不断调整成员变量和属性的命名,即可验证赋值顺序,总结如下

  • 1.优先通过setter方法,进行属性设置,调用顺序为:setName,_setName,setIsName

  • 2.如果以上方法均未找到,且+ (BOOL)accessInstanceVariablesDirectly方法返回YES,则通过成员变量进行设置,顺序是:_name,_isName,name,isName

  • 3.如果以上方法均未找到,如果+ (BOOL)accessInstanceVariablesDirectly方法返回NO,则直接调用setValue:forUndefinedKey:
    可通过案例进行验证,这里不再展示。

5.流程图

在这里插入图片描述

6.取值顺序

截取部分文档内容
在这里插入图片描述

参考取值顺序的分析思路,得出结论如下:

  • 1.优先通过getter方法,进行属性取值,调用顺序为:getName,name,isName,_name

  • 2.如果以上方法均未找到,且+ (BOOL)accessInstanceVariablesDirectly方法返回YES,则通过成员变量进行设置,顺序是:_name,_isName,name,isName

  • 3.如果以上方法均未找到,如果+ (BOOL)accessInstanceVariablesDirectly方法返回NO,则直接调用valueForUndefinedKey:
    可通过案例进行验证,这里不再展示。

7.关于valueForUndefinedKey

其实在前面的注释种也有说明,无论是在设值或者是取值时,如果没有找到对应的key,就会调用valueForUndefinedKey:,其实也就是在这里进行报错,也就是我们开发经常遇到的崩溃。
所以如果我们重写这个方法做一些处理就可以防止崩溃

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

包括前面提到的不能传nil,用NSNull代替,也可以在对应的方法做判断

//假设调用-setValue:forKey:将无法设置键控值,因为相应的访问器方法的参数类型是NSNumber标量类型或NSValue结构类型,但值为nil,请使用其他机制设置键控值。此方法的默认实现会引发NSInvalidArgumentException。您可以重写它,将nil值映射到应用程序上下文中有意义的内容。
- (void)setNilValueForKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"xxx"]) {
        [self setValue:@"" forKey:@”age”];
        //其实就是跳过了崩溃的执行
    } else {
        [super setNilValueForKey:key];
    }
}

二.自定义KVC

思路:参考官方文档给的设值和取值规则,以及一些特殊情况的处理,就可以模仿流程去实现自定义KVC,代码如下

@interface NSObject (YCXKVC)

// KVC 自定义入口
- (void)ycx_setValue:(nullable id)value forKey:(NSString *)key;
@end
#import "NSObject+YCXKVC.h"
#import <objc/runtime.h>

@implementation NSObject (YCXKVC)

- (void)ycx_setValue:(nullable id)value forKey:(NSString *)key{
   
    // KVC 自定义
    // 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 ycx_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self ycx_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self ycx_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
    // 3:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"YCXUnknownKeyException" 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:@"YCXUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    
}


- (nullable id)ycx_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:判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"YCXUnknownKeyException" 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)ycx_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

今天的分享就到这里,下一篇章KVO原理分析我们不见不散

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值