iOS底层-KVC原理

前言

在日常的开发中,我们通常会用到KVC进行赋值,或者访问一些私有属性。那么KVC是什么,它的原理又是怎样的?接下来一起去探究分析下。

KVC简介

  • Key-value coding(键值编码)是一种由NSKeyValueCoding非正式协议启用的机制,对象采用它来提供对其属性的间接访问。当一个对象符合键值编码时,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数来寻址。这种间接访问机制补充了实例变量及其关联的访问方法所提供的直接访问。
  • 查看setValue:forKey:源码,最终到Foundatin框架的NSKeyValueCoding文件:

截屏2021-08-01 23.33.39.png

Foundation是不开源的,所以只能查看KVC的官方文档:Key-Value Coding Programming Guide

KVC的复制和取值

先确定要看的位置,根据文档找到了Accessor Search Patterns
截屏2021-08-03 22.45.15.png

  • 先来看setter方法

Basic Setter

  • 文档上说调用setValue:forKey:对属性进行赋值时,
      1. 会按顺序去找set<Key>或者_set<Key>的方法,如果找到,就对它赋值,
      1. 如果没找到,如果类方法accessinstancevariablesdirect返回值为YES,就会按顺序去找实例变量_<key>_is<Key><key>, 或者 is<Key>,找到后并对它赋值
      1. 如果没有找到就会走setValue:forUndefinedKey:方法

代码验证

    1. 先定义一个LGPerson类,并设置实例变量,实现setName_setName方法,最后在ViewController中调用setValue:forKey:方法:
// .h
@interface LGPerson : NSObject {
@public
    NSString *_isName;
    NSString *name;
    NSString *isName;
    NSString *_name;
}

// .m
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

// ViewController.m
LGPerson *person = [[LGPerson alloc] init];
[person setValue:@"wushuang" forKey:@"name"];
复制代码

打印结果如下:

截屏2021-08-03 23.33.18.png

说明调用setValue:forKey:方法会先走setName方法,将setName方法注释掉,留下_setName,再运行:

截屏2021-08-03 23.35.36.png

这次走了_setName的方法,也就是说调用setValue:forKey:后,会先找setName方法,没找到再找_setName

  • setName_setName都不实现时会去找实例变量_name_isNamename, 或者 isName

猜想:此时多了isName_isName,那么setIsName_setIsName方法会不会走呢?接下来去验证下:

  • 先注释前面的两个set方法,然后添加setIsName_setIsName方法:
- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
复制代码
  • 运行结果发现走了setIsName方法,并没有走_setIsName,也就得到了新流程:

新流程:在调用setValue:forKey:后查找顺序为 setName -> _setName -> setIsName

  • set相关方法没找到就会去找相关的实例变量,先注释掉set相关方法,然后先实现类方法accessInstanceVariablesDirectly,返回值为YES,再打印实例变量的值:
// LGPerson.m
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

// ViewController.m
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
复制代码

运行结果:

截屏2021-08-03 23.53.17.png

  • 会先查找_name,注释掉最先打印的实例变量然后打印,最终得到实例变量的打印顺序:_name -> _isName -> name -> isName
  • 注释掉4个实例变量,然后实现方法setValue:forUndefinedKey在打印:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"%s -- %@ --- %@", __func__, value, key);
}
复制代码

结果如下:

截屏2021-08-04 00.01.05.png

  • 证明了当set方法实例变量都没有时,就会走setValue:forUndefinedKey方法。

流程图

  • Basic Setter流程如下:

截屏2021-08-04 00.21.24.png

Basic Getter

再来看看取值Search Pattern for the Basic Getter

截屏2021-08-04 06.38.52.png

  • 也就是实现valueForKey:方法后,系统会走以下几个步骤(暂不考虑集合类型):
      1. 在实例方法中搜索第一个名称为getNamenameisName_name的访问器方法。如果找到了,就调用它
      1. 如果没找到(除去集合类型),先检验类方法accessInstanceVariablesDirectly实现,并返回YES,然后依次搜索实例变量_name_isNamename,或isName,如果找到就直接获取实例变量的值并返回
      1. 如果没有找到就会走valueForUndefinedKey:方法

代码验证

先将set相关代码都注释,然后实现第一个步骤:

// LGPerson.m
- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

// ViewController.m
NSLog(@"取值: %@",[person valueForKey:@"name"]);
复制代码
  • 然后打印出一个后,注释掉打印的再运行,得到第一步实例方法的取值顺序:getName -> name -> isName -> _name

然后注释掉实例方法,再将实例变量赋值:

// ViewController.m
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";

NSLog(@"取值: %@",[person valueForKey:@"name"]);
复制代码
  • 然后打印出来一个,注释掉一个实例变量的赋值实例变量,再打印,得到第二步实例变量的取值顺序:_name -> _isName -> name -> isName
  • 再注释掉实例变量和实例变量的赋值,然后在LGPerson.m实现valueForUndefinedKey:方法:
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"%s --- %@", __func__, key);
    return NSStringFromSelector(_cmd);
}

// ViewController.m
NSLog(@"取值: %@",[person valueForKey:@"name"]);
复制代码
  • 运行结果如下:

截屏2021-08-04 07.33.02.png

至此,三步的取值流程都得以验证

流程图

  • Basic Setter查找流程如下:

截屏2021-08-04 09.13.17.png

自定义KVC

  • 了解了KVCsettergetter特性后,那我们自己能定义KVC吗?答案是肯定的。
  • 根据NSKeyValueCoding中的settergetter方法,在定义的NSOjbect分类中定义新的settergetter方法,然后定义WSPerson,里面有set实例方法和实例变量:
@interface NSObject (WSKVC)

- (void)ws_setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)ws_valueForKey:(NSString *)key;
@end

// WSPerson.h
@interface WSPerson : NSObject {
@public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end

// WSPerson.m
@implementation WSPerson
+ (BOOL)accessInstanceVariablesDirectly {
    return true;
}
- (void)setName:(NSString *)name {
    NSLog(@"%s --- %@", __func__ ,name);
}
- (void)_setName:(NSString *)name {
    NSLog(@"%s --- %@", __func__ ,name);
}
- (void)setIsName:(NSString *)name {
    NSLog(@"%s --- %@", __func__ ,name);
}
@end
复制代码
  • 接下来根据前面分析的取值和赋值流程对自定义方法进行相关处理

赋值ws_setValue:forKey:

代码如下:

- (void)ws_setValue:(nullable id)value forKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        return;
    }
    NSString *Key = key.capitalizedString; //首字母大写
    // 拼接相关的方法名
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];

    // 按顺序判断是否实现三个实例方法
    if ([self ws_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"___ %@ ___",setKey);
        return;
    } else if ([self ws_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"___ %@ ___",_setKey);
        return;
    } else if ([self ws_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"___ %@ ___",setIsKey);
        return;
    }

    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"WS_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }

    // 获取所有实例变量的名字
    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);
       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;
    }

    // 如果找不到相关实例变量,就抛出异常
    @throw [NSException exceptionWithName:@"WS_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self, NSStringFromSelector(_cmd)] userInfo:nil];
}

- (BOOL)ws_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;
}
复制代码
    1. 先判断key是否存在
    1. 再按顺序判断相关的set方法:setName: -> _setName -> setIsName
    • 先拼接出三个方法的名字,再依次调用respondsToSelector方法判断能不能响应,如果能响应就用performSelector方法执行调用
    1. 如果类方法accessInstanceVariablesDirectly返回值为NO,则抛出异常
    1. 如果返回YES,则先获取类的实例变量名字数组,然后根据相关实例方法的名字按顺序判断是否在数组中,如果存在就获取实例变量ivar,然后对它赋值
    1. 当实例方法也找不到值时,再抛出异常

取值ws_valueForKey:

代码如下:

- (nullable id)ws_valueForKey:(NSString *)key {
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 首字母大写
    NSString *Key = key.capitalizedString;
    // 拼接 方法名字,部分和实例变量名字相同
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    NSString *_key = [NSString stringWithFormat:@"_%@",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(isKey)]) {
        return [self performSelector:NSSelectorFromString(isKey)];
    } 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

    // 判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"WS_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    // 获取实例变量名字
    NSMutableArray *mArray = [self getIvarListName];

    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);;
    }
    @throw [NSException exceptionWithName:@"WS_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self, NSStringFromSelector(_cmd)] userInfo:nil];
    return @"";
}
复制代码
    1. 先判断key是否存在,不存在或者为空时,返回nil
    1. 再按顺序判断相关的get实例方法:getName: -> name -> isName -> _name
    • 先拼接出这几个方法的名字,再依次调用respondsToSelector方法判断能不能响应,如果能响应就用performSelector方法执行调用
    1. 如果类方法accessInstanceVariablesDirectly返回值为NO,则抛出异常
    1. 如果返回YES,则先获取类的实例变量名字数组,然后根据相关实例方法的名字按顺序判断是否在数组中,如果存在就获取实例变量ivar,然后取值
    1. 当实例方法也找不到值时,再抛出异常并返回空
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享