iOS KVC分析

1,258 阅读8分钟

序言

2022年4月初,面试的时候被KVC的相关问题尬了。当时被问到KVC的时候有种懵逼的感觉,可以说大概都知道,但是面试没准备,对这KVC的东西都有种朦胧的感觉,全都是 set<key>_set<key>_<key>is<key>这些,没有一个具体的概念。 现在有时间就查了一些资料,然后在这里做个记录。

一、体验KVC

体验直接开始代码感受一下,如下所示:

1、基本类型

LGPerson.h:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END

LGPerson.m:

#import "LGPerson.h"

@implementation LGPerson

@end

KVC的体验:

LGPerson *p = [LGPerson new];

p.name = @"zlg";

[p setValue:@"leilei" forKey:@"name"];

NSLog(@"%@",p.name);

输出结果:

20220507092513.jpg

输出结果是leilei而不是zlg

2、集合类型

LGPerson.h加入数组:

@property (nonatomic, strong) NSArray *array;

体验:

p.array = @[@"0",@"1",@"2",@"3",@"4"];

// 1、直接新数组赋值

NSArray *array = @[@"2",@"3",@"4"];

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

NSLog(@"直接新数组赋值: %@", p.array);

// 2、拷贝赋值

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

mArray[0] = @"0";

NSLog(@"拷贝赋值: %@", p.array);

输出结果:

20220507094929.jpg

简单的体验一下KVC。

二、认识KVC

KVC是NSKeyValueCoding非正式协议支持的一种机制,对象采用这种机制来提供对其属性的间接访问。当一个对象符合键值编码时,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数寻址。这种间接访问机制补充了实例变量及其关联访问器方法所提供的直接访问。

这段引用自官方文档

那么KVC是赋值和取值的机制是怎么样的呢?在官方文档里有这么一个解释简单翻译一下:

  1. 赋值
  • 查找方法:按照set<Key>: 和 _set<Key>的顺序查找
  • 查找实例变量:判断accessInstanceVariablesDirectly返回值为YES,然后按照_<key>_is<Key><key>, 和 is<Key>的顺序查找
  1. 取值
  • 查找方法:按照get<Key><key>is<Key>, 和 _<key>的顺序查找
  • 查找集合方法(不探究):countOf<Key>, objectIn<Key>AtIndex:<key>AtIndexes:countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:countOf<Key>, enumeratorOf<Key>, memberOf<Key>:
  • 查找实例变量:判断accessInstanceVariablesDirectly返回值为YES,然后按照_<key>_is<Key><key>, 和 is<Key>的顺序查找

accessInstanceVariablesDirectly这个方法在KVC的取值和赋值中都有用到,我们看一下官方给它的解释:

如果KVC方法的默认实现在没有找到属性的访问方法时应该直接访问对应的实例变量,则返回YES。如果不应该返回NO。NSObject对这个方法的实现返回YES。子类可以重写它以返回NO,在这种情况下,其他方法将不会访问实例变量。

所以它默认的返回值是YES

2.1、赋值验证

2.1.1、方法查找验证

LGPerson.h中的name属性注销 LGPerson.m中实现setName:_setName:方法,如下:

#import "LGPerson.h"

@implementation LGPerson

- (void)setName:(NSString *)name{

    NSLog(@"%s--%@", __func__, name);

}

- (void)_setName:(NSString *)name{

    NSLog(@"%s--%@", __func__, name);

}

@end

然后调用:

LGPerson *p = [LGPerson new];

[p setValue:@"leilei" forKey:@"name"];

运行结果: image.png 走了setName:方法,注释此方法再次运行,结果如下: image.png 这一次走到了_setName:方法, 验证了赋值的方法查找。

2.1.2、实例变量查找验证

将方法注释了,添加4个实例变量_name_isNamenameisName并实现 LGPerson.h如下:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject {

    @public

    NSString *_name;

    NSString *_isName;

    NSString *name;

    NSString *isName;

}

//@property (nonatomic, copy) NSString *name;

@property (nonatomic, strong) NSArray *array;

@end

NS_ASSUME_NONNULL_END

LGPerson.m如下:

#import "LGPerson.h"

@implementation LGPerson

+ (BOOL)accessInstanceVariablesDirectly{

    return YES;

}

//- (void)setName:(NSString *)name{

//    NSLog(@"%s--%@", __func__, name);

//}

//- (void)_setName:(NSString *)name{

//    NSLog(@"%s--%@", __func__, name);

//}

@end

调用:

    LGPerson *p = [LGPerson new];

    [p setValue:@"leilei" forKey:@"name"];

    NSLog(@"_name:%@",p->_name);

    NSLog(@"_isName:%@",p->_isName);

    NSLog(@"name:%@",p->name);

    NSLog(@"isName:%@",p->isName);

运行结果: image.png 直接抛出异常,说明将accessInstanceVariablesDirectly的返回值设为NO不支持实例变量的查找,将将accessInstanceVariablesDirectly的返回值设为YES再次运行:

image.png_name有值。注释_name后运行结果:

image.png_isName有值。注释_isName后运行结果:

image.pngname有值。注释name后运行结果:

image.png 在只有isName的情况下才会给isName赋值。 至此赋值即setValue:forKey:的验证结束,得出结论:

KVC在使用setValue:forKey:赋值时,先查找set<key>然后查找_set<key>这2个方法,如果未找到则会判断accessInstanceVariablesDirectly返回值,为NO直接抛出异常,为YES继续查找实例变量,查找顺序为_name_isNamenameisName

2.2、取值验证

2.2.1、方法查找验证

首先把LGPerson.h中的方法全部注释,然后在LGPerson.m添加getNamenameisName_nameaccessInstanceVariablesDirectly返回值为NO,如下所示:

#import "LGPerson.h"

@implementation LGPerson

+ (BOOL)accessInstanceVariablesDirectly{

    return YES;

}

//- (void)setName:(NSString *)name{

//    NSLog(@"%s--%@", __func__, name);

//}

//- (void)_setName:(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

调用如下:

LGPerson *p = [LGPerson new];

NSLog(@"取到的值%@",[p valueForKey:@"name"]);

运行结果如图:

image.png 取到的值是getName,注释该方法继续运行:

image.png 取到的值是name,注释该方法继续运行:

image.png 取到的值是isName,注释该方法继续运行:

image.png 只有_name方法,此时取到的值为_name

2.2.2、实例变量查找验证

添加4个实例变量_name_isNamenameisName并实现accessInstanceVariablesDirectly返回值为直接设为NO,跟赋值时候类似(见2.1.2) 调用如下:

LGPerson *p = [LGPerson new];

p->name = @"name";

p->_name = @"_name";

p->_isName = @"_isName";

p->isName = @"isName";

NSLog(@"取到的值%@",[p valueForKey:@"name"]);

运行结果如下:

image.png 同样抛出异常,将accessInstanceVariablesDirectly返回值为直接设为YES

image.png 取到的值为_name,注释后再次运行:

image.png 取到的值为_isName,注释后再次运行:

image.png 取到的值为name,注释后再次运行:

image.png 最后取到的值为isName。我们验证完毕,得出结论:

KVO取值即valueForKey:先查找方法,按照getNamenameisName_name的顺序进行查找。没有找到对应方法则会对accessInstanceVariablesDirectly进行判断,若为NO直接抛出异常,为YES则继续进行实例变量查找,查找顺序为_name_isNamenameisName

2.3、KVC防护

实际上在前面的验证中除了已经列出来的2个异常,在所有的查找方式都没结果之后都会直接抛出异常。如何避免呢?在官方的说明中有2个方法valueForUndefinedKey:setValue:forUndefinedKey:用来处理取值和赋值的时候找不到key而引发的异常,我们来简单的验证一下,在LGPerson.m中实现这2个方法,如下所示:

- (id)valueForUndefinedKey:(NSString *)key{

    NSLog(@"错误的key:%@",key);

    return nil;

}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{

    NSLog(@"错误的key:%@, 值:%@", key, value);

}

调用

LGPerson *p = [LGPerson new];

[p setValue:@"123" forKey:@"age"];

NSLog(@"取到的值%@",[p valueForKey:@"age"]);

并没有age相关的方法和实例变量,运行结果如下:

image.png 没有抛出异常,并且走了我们valueForUndefinedKey:setValue:forUndefinedKey:方法。这样我们就避免了KVC引起的异常。

3、自定义KVC

KVC的赋值与取值流程基本上已经探索完毕,过程也没有那么困难,所以我们来自定义一个我们自己的KVC。 首先创建一个LGKVC的分类,如下:

NSObject+LGKVC.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (LGKVC)

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

- (id)LG_valueForKey:(NSString *)key;

@end

NS_ASSUME_NONNULL_END

NSObject+LGKVC.m

#import "NSObject+LGKVC.h"

@implementation NSObject (LGKVC)

- (void)LG_setValue:(id)value forKey:(NSString *)key{

}

- (id)LG_valueForKey:(NSString *)key{

    return nil;

}

@end

声明我们自己的赋值和取值方法LG_setValue:forKey:LG_valueForKey:并实现空实现,接下来开始实现具体的自定义方法

3.1、自定义赋值方法

首先我们来确定自定义赋值方法的流程:

  1. 验证<key>是否存在
  2. setter方法查找:set<Key>:_set<Key>
  3. 判断accessInstanceVariablesDirectly的值,YES继续NO则抛出异常
  4. 查找实例变量_<key>_is<key><key>is<key>
  5. 未查找到抛出异常 具体实现如下:
#import "NSObject+LGKVC.h"

#import <objc/runtime.h>

@implementation NSObject (LGKVC)

- (void)LG_setValue:(id)value forKey:(NSString *)key{

    // 1、验证`<key>`是否存在
    if (key == nil || key.length == 0) {

        NSLog(@"key值不存在");

        return;

    }

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

    // 2. setter方法查找:`set<Key>:` 和 `_set<Key>`

    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];

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

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

        return;

    }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {

        return;

    }

    // 3. 判断accessInstanceVariablesDirectly的值,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.查找实例变量_<key>,_is<key>,<key>,is<key>
    NSMutableArray *mArray = [self getIvarListName];

    NSString *_key = [NSString stringWithFormat:@"_%@",key];

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

    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];

    if ([mArray containsObject:_key]) {

        // 获取相应的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);

        // 对相应的 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];

}

- (id)LG_valueForKey:(NSString *)key{

    return nil;

}

#pragma mark - 相关方法

//判断方法是否存在并调用
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{

    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {

        NSLog(@"找到方法:%@",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;

}

@end

简单测试一下:

LGPerson *p = [LGPerson new];

[p LG_setValue:@"leilei" forKey:@"name"];

运行结果:

image.png 调用lg_performSelectorWithMethodName并找到setName:且调用了。后面几个方法查找就不一一验证了。

3.2、自定义取值方法

与赋值方法一样,先来确定流程:

  1. 验证<key>是否存在
  2. getter方法查找:get<Key><key>countOf<Key>objectIn<Key>AtIndex
  3. 判断accessInstanceVariablesDirectly的值,YES继续NO则抛出异常
  4. 查找实例变量_<key>_is<key><key>is<key>
  5. 未查找到抛出异常 具体实现如下:
- (id)LG_valueForKey:(NSString *)key{

    // 1、验证`<key>`是否存在
    if (key == nil  || key.length == 0) {

        return nil;

    }

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

    // 2. setter方法查找:`set<Key>:` 和 `_set<Key>`

    // 拼接方法
    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:@"LGUnknownKeyException" 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>`
    NSMutableArray *mArray = [self getIvarListName];

    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);;

    }
    // 5.抛出异常
    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];

    }
    return @"";

}

简单验证一下,代码如下:

LGPerson *p = [LGPerson new];

NSLog(@"取到的值%@",[p valueForKey:@"name"]);

运行结果:

image.png 后面的也不再一一验证。

至此一个简单的自定义KVC就完成了。