「OC」探索 KVC 的基础与应用

「OC」KVC的初步学习

前言

在我们前面总结了五大传值方法,其中在KVO之中,我们是用 observeValueForKeyPath:ofObject:change:context:的方法实现对对象的属性进行监听的,其中我们想要对属性监听需要我们用到一个KeyPath的参数,去找到监听对象之中与KeyPath名字相同的对应属性。KVC和KVO有些相似,KVC需要使用键值对去直接访问对应的属性

介绍

KVC的相关方法

KVC(Key-value coding)键值编码,指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值而不需要调用明确的存取方法。我们可以不使用getter/setter的方法间接访问对象当中的属性。这个间接,延伸来说就是可以破坏属性之中只读的特性,也可以直接访问到类之中的私有属性,一定程度上使用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来设值

点击进入任意一个方法,我们可以看到

image-20240920114044998

这几个方法全都是在NSObject当中的NSKeyValueCodin分类写的,所以对于所有继承于NSObject之中的所有OC对象来说,都可以使用KVC的方法。

key和keyPath的区别

  • key(键)key 是一个字符串,用于标识对象中的属性。通过 valueForKey:setValue:forKey: 这样的方法,可以使用 key 来访问或设置对象的属性值。
  • keyPath(键路径)keyPath 是由多个 key 连接而成的路径,用于访问对象的嵌套属性。通过 valueForKeyPath:setValue:forKeyPath: 这样的方法,可以沿着路径访问嵌套属性(即访问对象属性之中的属性)。

以下是代码示例

#import "ViewController.h"

@interface Address : NSObject

@property (nonatomic, strong) NSString *city;

@end

@interface Person : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, strong) Address *address;

@end


@implementation Person
@end

@implementation Address
@end

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
 		Person *person = [[Person alloc] init];
    [person setValue:@"bb" forKey:@"name"];
    [person setValue:@30 forKey:@"isAge"];
    
    Address *address = [[Address alloc] init];
    [address setValue:@"Xi'An" forKey:@"city"];
    [person setValue:address forKey:@"address"];

    // 使用 key 访问属性
    NSString *name = [person valueForKey:@"name"];
    NSNumber *age = [person valueForKey:@"age"];

    // 使用 keyPath 访问嵌套属性
    NSString *city = [person valueForKeyPath:@"address.city"];
    
    NSLog(@"%@ %@",name, age);
    NSLog(@"%@", city);
}


@end

image-20240920121759817

KVC的其他方法

//默认返回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;

setValue:forKey原理

在这里插入图片描述

使用以下代码进行实验

-(void)setAge:(NSNumber *)age {
    NSLog(@"1");
    _age = age;
}

image-20240920212020720

通过实验我发现,在用@property创建属性的时候,使用KVC的时候似乎不会对_setKey进行查找,另外一个有趣的点,如果我们在属性之中重写属性方法的名称,如下

@property (nonatomic, strong, setter = makeName:) NSString *name;

然后像上面一下写一个setter方法,那么KVC不会检测到这个重写后到setter,会直接执行后面的步骤

-(void)makeName:(NSString *)name {
    _name = name;
    NSLog(@"1");
}

接着,如果找不到对应的setter方法的话,就要执行accessInstanceVariablesDirectly这个方法,当这个方法返回 YES 时,KVC 将会优先访问对象的实例变量;返回 NO 时,KVC直接调用setValue:forUndefinedKey:,抛出异常

KVC在得到为YES的值之后,就会直接对寻找成员变量对其进行赋值,依照KeyisKey的顺序进行查找。

模拟实现setValue:forKey
//设值
- (void)jc_setValue:(nullable id)value forKey:(NSString *)key{
    
//    1、判断key 是否存在
    if (key == nil || key.length == 0) return;
    
//    2、找setter方法,顺序是:setXXX、_setXXX、 setIsXXX
    // key 要大写
    NSString *Key = key.capitalizedString;
    // key 要大写
    NSString *setKey = [NSString stringWithFormat:@"set%@:", Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key];
    
    if ([self performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*************%@*************", setKey);
        return;
    }else if([self performSelectorWithMethodName:_setKey value:value]){
        NSLog(@"*************%@*************", _setKey);
        return;
    }else if([self performSelectorWithMethodName:setIsKey value:value]){
        NSLog(@"*************%@*************", setIsKey);
        return;
    }
    
    
//    3、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"UnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
//    4、间接访问变量赋值,顺序为:_key、_isKey、key、isKey
    // 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:@"UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    
}

ValueforKey原理

在这里插入图片描述

这个方法和上面那个方法类似,只不过这个方法为getter,探究的过程和上面的方法类似,只是多了一个检查容器类和集合类的操作

  • 1、按照getKey,key,isKey,_key的顺序查找成员方法,如果找到直接调用取值

  • 2、如果步骤1没有找到的话。对于容器类属性来说,KVC 会依次查找以下三个方法的组合:

    • countOf<Key>:返回集合元素数量(类似 NSArraycount
    • objectIn<Key>AtIndex::根据索引获取单个元素(类似 objectAtIndex:
    • <key>AtIndexes::根据索引集合批量获取元素(类似 objectsAtIndexes:
    • 若找到 countOf<Key> 和另外两个方法之一,KVC 会生成一个 NSKeyValueArray 代理对象NSArray 的子类),该对象会将所有 NSArray 方法调用(如 countobjectAtIndex:)转换为对上述三个方法的组合调用。

    3、对于集合类来说,KVC依次查找以下三个方法的组合:

    • countOf<Key>:返回集合元素数量
    • enumeratorOf<Key>:返回遍历集合的迭代器(类似 NSSetobjectEnumerator
    • memberOf<Key>::检查元素是否存在(类似 member:
    • 若三个方法均存在,KVC 生成一个 NSKeyValueSet 代理对象NSSet 的子类),将 NSSet 方法(如 countcontainsObject:)转换为对这三个方法的调用。
  • 4、如果没有找到,查看accessInstanceVariablesDirectly的返回值

  • 返回值为YES,按照_Key,_isKey,Key,isKey的顺序查找成员变量,如果找到,直接取值,如果没有找到,调用setValue:forUndefinedKey:,抛出异常

  • 返回NO,直接调用setValue:forUndefinedKey:,抛出异常

查找容器属性
@interface JCClass : NSObject
- (NSUInteger)countOfItems;
- (id)objectInItemsAtIndex:(NSUInteger)index;

@end

@implementation JCClass {
    NSMutableArray *_internalItems;
}

- (instancetype)init {
    if (self = [super init]) {
        _internalItems = [NSMutableArray arrayWithObjects:@1, @2, @3, nil];
    }
    return self;
}

#pragma mark 必须实现的方法
- (NSUInteger)countOfItems {
    return _internalItems.count;
}

- (id)objectInItemsAtIndex:(NSUInteger)index {
    return _internalItems[index];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        JCClass *jcInstance = [[JCClass alloc] init];

        // 通过 KVC 获取代理数组(无需直接访问 internalItems)
        NSArray *proxyArray = [jcInstance valueForKey:@"items"];
        NSLog(@"初始数组: %@", proxyArray); 
        
        
    }
    return 0;
}
模拟实现
//取值
- (nullable id)jc_valueForKey:(NSString *)key{
    
//    1、判断非空
    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、判断是否响应`accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"UnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
//    4.找相关实例变量进行赋值,顺序为:_<key>、 _is<Key>、 <key>、 is<Key>
    // 4.1 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // 例如:_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 @"";
}

在集合之中KVC的用法

在我们遇上对集合使用KVC的时候,可以用以下方法

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath

- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key 
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath

这些方法就是使用KVC是获取对象之中的集合属性,以下是简单示例

1. mutableArrayValueForKey:mutableArrayValueForKeyPath:
// 获取可变数组对象
NSMutableArray *mutableArray = [object mutableArrayValueForKey:@"arrayProperty"];

// 使用 Key-Value Path 获取可变数组对象
NSMutableArray *nestedMutableArray = [object mutableArrayValueForKeyPath:@"nestedObject.arrayProperty"];
2. mutableSetValueForKey:mutableSetValueForKeyPath:
// 获取可变集合对象
NSMutableSet *mutableSet = [object mutableSetValueForKey:@"setProperty"];

// 使用 Key-Value Path 获取可变集合对象
NSMutableSet *nestedMutableSet = [object mutableSetValueForKeyPath:@"nestedObject.setProperty"];
3. mutableOrderedSetValueForKey:mutableOrderedSetValueForKeyPath:
// 获取可变有序集合对象
NSMutableOrderedSet *mutableOrderedSet = [object mutableOrderedSetValueForKey:@"orderedSetProperty"];

// 使用 Key-Value Path 获取可变有序集合对象
NSMutableOrderedSet *nestedMutableOrderedSet = [object mutableOrderedSetValueForKeyPath:@"nestedObject.orderedSetProperty"];

那么这些方法的运用场景是什么呢?根据大佬的博客,是和我们前面讲到KVO有些相关,因为我们单纯向可变集合当中添加元素,并不会使得数组的首地址出现任何改变,KVO无法对可变集合的改变进行监听,那么除了我们手动对KVO进行监听之外,我们就可以用上这个相对应的方法使用KVC进行监听。

使用- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath这个方法可以使得可变数组的地址发生改变,也就是完成一次深层拷贝。

在字典之中KVC的用法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

用对字典类型使用KVC其实更多用在网络请求之中,因为网络请求的JSON数据是以字典的类型进行返回,有时候一个对象的属性有很多个就不太方便我们使用平时普通的赋值方法,太过于复杂了。我们对于Model类来说,用以下方式会更加方便

这个属性最常用到的地方就是字典转模型 例如我们有一个Student类,

@interface Student : NSObject
@property (nonatomic,assign) float height;
@property (nonatomic,assign) int age;
@property (nonatomic,strong) NSString *name;
@end

使用setValuesForKeysWithDictionary方法呢

Student *stu = [[Student alloc]init];
//在进行网络请求之后得到的数据
NSDictionary *dic = @{@"name":@"jack",@"height":@180,@"age":@10};
[stu setValuesForKeysWithDictionary:dic];

KVC的操作符

KVC可以调用相对应的操作符,像这样的函数共有5个@avg,@count,@max,@min,@sum。顾名思义,分别就是平均数,数组大小,最大值,最小值,总和,用法大致如下:

  1. @avg:计算集合中指定属性的平均值。
NSArray *numbers = @[@10, @20, @30, @40];
NSNumber *average = [numbers valueForKeyPath:@"@avg.self"];
NSLog(@"Average: %@", average);
  1. @count:计算集合中元素的数量。
NSArray *numbers = @[@10, @20, @30, @40];
NSNumber *count = [numbers valueForKeyPath:@"@count"];
NSLog(@"Count: %@", count);
  1. @max:计算集合中指定属性的最大值。
NSArray *numbers = @[@10, @20, @30, @40];
NSNumber *maxValue = [numbers valueForKeyPath:@"@max.self"];
NSLog(@"Max Value: %@", maxValue);
  1. @min:计算集合中指定属性的最小值。
NSArray *numbers = @[@10, @20, @30, @40];
NSNumber *minValue = [numbers valueForKeyPath:@"@min.self"];
NSLog(@"Min Value: %@", minValue);
  1. @sum:计算集合中指定属性的总和。
NSArray *numbers = @[@10, @20, @30, @40];
NSNumber *sum = [numbers valueForKeyPath:@"@sum.self"];
NSLog(@"Sum: %@", sum);

如何抛出异常

前面我们说到,如果KVC找不到对应的属性的话,KVC就会进行异常抛出。

在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash。我们可以重写下面两个方法,根据业务需求合理的处理KVC导致的异常。

- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;

其中重写这两个方法,在key值不存在的时候,会走下面方法,而不会异常抛出

- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

在 Objective-C 中,这些方法是用于处理对象的未定义键(Undefined Key)的情况的方法。通常,当您尝试访问或设置一个对象中不存在的键时,系统会调用这些方法。以下是这些方法的简要说明以及如何操作它们:

1. - (nullable id)valueForUndefinedKey:(NSString *)key;

这个方法在尝试获取对象中不存在的键的值时被调用。我们可以通过实现这个方法来自定义对象对未定义键的行为。

- (nullable id)valueForUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"undefinedKey"]) {
        // 返回一个默认值或者处理逻辑
        return @"Default Value";
    } else {
        // 调用父类的默认行为
        return [super valueForUndefinedKey:key];
    }
}

2. - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

这个方法在尝试为对象中不存在的键设置值时被调用。我们可以实现这个方法来定义对象对未定义键设置值的行为。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"undefinedKey"]) {
        // 自定义处理逻辑,例如忽略或记录警告
        NSLog(@"Attempted to set a value for an undefined key: %@", key);
    } else {
        // 调用父类的默认行为
        [super setValue:value forUndefinedKey:key];
    }
}

参考文章

KVC

KVC基本原理和用法

iOS-底层原理 22:KVC 底层原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值