深入理解KVC(Key-Value Coding)实现机制

KVC苹果官方文档链接:Key-Value Coding Programming Guide
KVC(Key-Value Coding)键值编码是一种通过NSKeyValueCoding非正式的协议,间接访问对象的属性的机制。
在iOS开发中,开发者可以通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取(getter和setter)方法。这样就可以在运行时动态地访问和修改对象的属性。

KVC的定义是对NSObject的扩展来实现的,在Objective-C中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型的类,其对象都能使用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类别中还有其他的一些方法


1. +(BOOL)accessInstanceVariablesDirectly; //默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索

 2. -(BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; //KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。

 3. -(NSMutableArray *)mutableArrayValueForKey:(NSString *)key; //这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。

 4. -(nullable id)valueForUndefinedKey:(NSString *)key; //如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。

 5. -(void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; //和上一个方法一样,但这个方法是设值。

 6. -(void)setNilValueForKey:(NSString *)key; //如果你在SetValue方法时面给Value传nil,则会调用这个方法

 7. -(NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。

KVC是如何寻找key的
我们来探讨一下KVC在内部是按什么样的顺序来寻找key的,相信大家经常使用- (void)setValue:(nullable id)value forKey:(NSString *)key这个方法,
当调用setValue:属性值 forKey:@”name“的代码时,底层的执行机制如下:

1、 首先调用setter方法,setter方法有两个(setKey:和setIsKey:)。
其中Key表示属性名,首字母大写,两个方法执行顺序为先通过key找setKey:方法,如果没有找到,则去找setIsKey:方法。
2、如果没有找到setter方法,则去检查下面的方法返回值
+ (BOOL)accessInstanceVariablesDirectly
默认该方法会返回YES,表示可以直接访问实例的成员变量。
如果你重写返回NO,则不可以访问实例的成员变量,KVC会执行setValue:forUndefinedKey:方法(控制台报错,没有找到对应的key),一般开发者不会重写该方法返回NO。
这里写图片描述

    接下来我们来看一下,返回YES的情况下,是如何查找key。找成员变量 先后顺序是:_key -> _isKey -> key ->isKey
先去查找是否有_key的变量,如果没有则去查找是否有_isKey的变量,如果没有则去查找是否有key的变量,如果还没有则去查找是否有isKey的变量,isKey没有后则会执行setValue:forUndefinedKey:抛出异常。如果以上四个变量找到其中任何一个,KVC都可以对该成员变量赋值。

#import "Person.h"
@implementation Person
{
    /**
     在没有找到对应的setter方法后,会先看+(BOOL)accessInstanceVariablesDirectly的返回值
     如果返回YES,则查找key,返回NO,执行setValue:forUndefinedKey:抛异常
     KVC中找key的优先级:_name → _isName →  name → isName
     如果这几个成员变量都没有,执行setValue:forUndefinedKey:抛出异常
     */
    NSString *_name;
    NSString *_isName;
    NSString *isName;
    NSString *name;
}

/**
 1、 首先调用setter方法,setter方法有两个(setKey:和setIsKey:)。
 其中Key表示属性名,首字母大写,两个方法执行顺序为先通过key找setKey:方法,如果没有找到,则去找setIsKey:方法。
 2、如果没有找到setter方法,则去检查下面的方法返回值
 + (BOOL)accessInstanceVariablesDirectly
 默认该方法会返回YES,表示可以直接访问实例的成员变量。
 如果你重写返回NO,则不可以访问实例的成员变量,KVC会执行setValue:forUndefinedKey:方法(控制台报错,没有找到对应的key),一般开发者不会重写该方法返回NO。
 */
-(void)setName:(NSString *)name{
    _name = name;
    NSLog(@"-(void)setName:(NSString *)name被执行了");
}
-(void)setIsName:(NSString *)name{
    _name = name;
    NSLog(@"-(void)setIsName:(NSString *)name被执行了");
}

+(BOOL)accessInstanceVariablesDirectly{
    return NO;
}

-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"赋值抛出异常,没有找到key");
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"_name = %@  _isName = %@, name = %@, isName = %@", _name, _isName, name, isName];
}

我们来验证一下赋值时寻找key的顺序,首先重写+(BOOL)accessInstanceVariablesDirectly返回NO,注释掉-(void)setName:(NSString )name和-(void)setIsName:(NSString )name两个方法,运行后出现如下错误
这里写图片描述
如果取消注释,赋值成功。
在没有对应的setter方法,+(BOOL)accessInstanceVariablesDirectly返回YES时,运行结果如下
这里写图片描述
可以看到优先对_name赋值,在这里你可以注释掉 _name,看接下来会优先查找哪个变量,验证后你会发现顺序是按照我们上面说的那样:KVC中找key的优先级:_name → _isName → name → isName

当调用valueforKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:
1、首先按getKey→key→isKey的顺序查找getter方法,找到的话会直接执行。如果是BOOL或者int等值类型, 会做NSNumber转换。
2、如果还没有找到,再检查类方法
+ (BOOL)accessInstanceVariablesDirectly,如果返回YES,那么和先前的设值一样,会按_key -> _isKey -> key ->isKey顺序搜索成员变量名,如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:
3、最后还没有找到成员变量的话,调用valueForUndefinedKey:抛出异常

在KVC中使用KeyPath

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值

在开发过程中,一个类的成员变量有可能是其他的自定义类,通过键路径KeyPath,可以很容易做到。
例如:Person类中有一个Dog类的对象,修改dog的颜色color

    Person *per = [[Person alloc] init];
    per.dog = [[Dog alloc] init];
    [per setValue:@"黑色" forKeyPath:@"dog.color"];//赋值color成黑色
    NSLog(@"%@", [per valueForKeyPath:@"dog.color"]);//输出color值

自定义实现KVC赋值机制
弄清楚KVC如何寻找key,按照寻找顺序,可以自己实现KVC,下面的代码是自定义的赋值

#import "NSObject+LRKVC.h"
#import <objc/runtime.h>
@implementation NSObject (LRKVC)
-(void)lr_setValue:(id)value forKey:(NSString *)key{
    //key值需要合法
    if (key == nil || key.length == 0) {
        return;
    }
    //value类型:如果不是id类型,抛异常
    if (![value isKindOfClass:[NSObject class]]) {
        NSException *exception = [NSException exceptionWithName:@"LRKVO" reason:@"must be NSObject type" userInfo:nil];
        @throw exception;
        return;
    }
    //调用相关setter方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:", key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(setKey)]) {

        [self performSelector:NSSelectorFromString(setKey) withObject:value];
        return;
    }
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(setIsKey)]) {
        [self performSelector:NSSelectorFromString(setIsKey) withObject:value];
        return;
    }

    //异常处理方法
    if (![self.class accessInstanceVariablesDirectly]) {
        NSException *exception = [NSException exceptionWithName:@"LRKVO" reason:@"你返回了一个NO" userInfo:nil];
        @throw exception;
    }
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    //_key
    for (int i = 0; i < count; i++) {
        Ivar ivar =ivars[i];
        NSString *keyName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@", key]]) {
            object_setIvar(self, ivar, value);
            free(ivars);
            return;
        }
    }
    //_isKey
    for (int i = 0; i < count; i++) {
        Ivar ivar =ivars[i];
        NSString *keyName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        if ([keyName isEqualToString:[NSString stringWithFormat:@"_is%@", key.capitalizedString]]) {
            object_setIvar(self, ivar, value);
            free(ivars);
            return;
        }
    }
    //key
    for (int i = 0; i < count; i++) {
        Ivar ivar =ivars[i];
        NSString *keyName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        if ([keyName isEqualToString:key]) {
            object_setIvar(self, ivar, value);
            free(ivars);
            return;
        }
    }
    //isKey
    for (int i = 0; i < count; i++) {
        Ivar ivar =ivars[i];
        NSString *keyName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        if ([keyName isEqualToString:[NSString stringWithFormat:@"is%@", key.capitalizedString]]) {
            object_setIvar(self, ivar, value);
            free(ivars);
            return;
        }
    }
    //异常处理方法
    [self valueForUndefinedKey:key];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值