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

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

KVC相关API

常用方法

  • 通过key设值/取值
//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;

//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
  • 通过keyPath(路由)设值/取值
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath; 

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

其他方法

//默认返回YES,表示如果没有找到Set方法的话,会按照_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 *)dictionaryWithValuesForKeys:(NSArray *)keys;

KVC设值的底层原理

在日常开发中,针对对象属性的赋值,一般有以下两种方式

  • 直接通过setter方法赋值
  • 通过KVC键值编码的相关API赋值
HTPerson *p = [[HTPerson alloc] init];
p.name = @"name";
[p setValue:@"kvcName" forKey:@"name"];

下面针对使用最多的KVC设值方法:setValue:forKey,来进行其底层原理的探索。

  • 首先进入setValue:forKey的声明,发现是在Foundation框架中,而Foundation框架是不开源的
image
  • 通过苹果官方文档Key-Value Coding Programming Guide,来研究KVC赋值流程
image

当调用setValue:forKey:设置属性value时,其底层的执行流程为

image

  • 【第一步】首先查找是否有这三种setter方法,按照查找顺序为set:--> _set: --> setIs:
    • 如果有其中任意一个setter方法,则直接设置属性的value(注意:key是指成员变量名,首字符大小写需要符合KVC的命名规范)
    • 如果都没有,则进入【第二步】
  • 【第二步】:如果没有第一步中的三个简单的setter方法,则查找accessInstanceVariablesDirectly返回值,返回NO,则进入【第三步】
    • 如果返回YES,则查找间接访问的实例变量进行赋值,查找顺序为:_ --> _is -> -> is
    • 如果找到其中任意一个实例变量,则赋值
    • 如果都没有,则进入【第三步】
  • 【第三步】如果setter方法 或者 实例变量都没有找到,系统会执行该对象的setValue:forUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

KVC取值底层原理

查看苹果文档Key-Value Coding Programming Guide,当调用valueForKey:时,其底层的执行流程为

image

  • 【第一步】首先查找getter方法,按照get --> --> is --> _的方法顺序查找
    • 如果找到了,就调用它,并使用结果继续【第五步】
    • 如果没有找到,则进入【第二步】
  • 【第二步】如果第一步中的getter方法没有找到,KVC会查找countOfobjectInAtIndex:AtIndexes:
    • 如果找到countOf和其他两个中的一个,则会创建一个响应所有NSArray方法的集合代理对象,响应所有NSArray方法返回该对象,即NSKeyValueArray,是NSArray子类。代理对象随后将接收到的所有NSArray消息转换为countOfobjectInAtIndex:AtIndexes:消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get:range:之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)
    • 如果没有找到这三个访问数组的,请继续进入【第三步】
  • 【第三步】如果没有找到上面的几种方法,则会同时查找countOfenumeratorOfmemberOf:这三个方法
    • 如果这三个方法都找到,则会创建一个响应所有NSSet方法的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOfenumeratorOfmemberOf:消息的某种组合,用于创建它的对象
    • 如果还是没有找到,则进入【第四步】
  • 【第四步】如果还没有找到,检查类方法InstanceVariablesDirectly是否YES,依次搜索__isis的实例变量
    • 如果搜到,直接获取实例变量的值,进入【第五步】
    • 如果类方法InstanceVariablesDirectly返回NO,进入【第六步】
  • 【第五步】根据搜索到的属性值的类型,返回不同的结果
    • 如果是对象指针,则直接返回结果
    • 如果是NSNumber支持的标量类型,则将其存储在NSNumber实例中并返回它
    • 如果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象
  • 【第六步】如果上面5步的方法均失败,系统会执行该对象的valueForUndefinedKey:方法,默认抛出NSUndefinedKeyException类型的异常

使用路由访问,即keyPath

在日常开发中,一个类的成员变量有可能是自定义类或者其他的复杂数据类型,一般的操作是,我们可以先通过KVC获取该属性,然后再通过KVC获取自定义类的属性,就是比较麻烦,还有另一种比较简便的方法,就是使用KeyPath路由,涉及以下两个方法:setValue:forKeyPath:valueForKeyPath:

//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  

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

KVC使用场景

1、动态设值和取值

  • 常用的可以通过setValue:forKey:valueForKey:
  • 也可以通过路由的方式setValue:forKeyPath:valueForKeyPath:

2、通过KVC访问和修改私有变量

在日常开发中,对于类的私有属性,在外部定义的对象,是无法直接访问私有属性的,但是对于KVC而言,一个对象没有自己的隐私,所以可以通过KVC修改和访问任何私有属性

3、多值操作(model和字典互转)

model和字典的转换可以通过下面两个KVC的API实现

//字典转模型
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

//模型转字典
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;

4、修改一些系统控件的内部属性

在日常开发中,我们知道,很多UI控件都是在其内部由多个UI控件组合而成,这些内部控件苹果并没有提供访问的API,但是使用KVC可以解决这个问题,常用的就是自定义tabbar、个性化UITextField中的placeHolderText

5、用KVC实现高阶消息传递

在对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是对容器本身进行操作,结果会被添加到返回的容器中,这样,可以很方便的操作集合 来返回 另一个集合

//KVC实现高阶消息传递
- (void)transmitMsg{
    NSArray *arrStr = @[@"english", @"franch", @"chinese"];
    NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];
    
    for (NSString *str in arrCapStr) {
        NSLog(@"%@", str);
    }
    
    NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
    for (NSNumber *length in arrCapStrLength) {
        NSLog(@"%ld", (long)length.integerValue);
    }
}
image

你可能感兴趣的:(iOS底层原理22:KVC底层原理)