iOS底层(十一)-KVC探索

347 阅读4分钟

一、KVC简介

我们在开发过程中经常会经历过这样的代码:

    Person p = [[Person alloc] init];
    p.name = @"Jack";

这么写是理所当然的, 会调用name的set方法. 我们还可以用KVC的方式来给属性设置值:

[person setValue:@"Jack" forKey:@"name"];

KVC的全程就是 key-value Coding. 在Documentation Archive中, 我们可以查到key value的相关介绍:

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

二、KVC的访问类型

访问对象属性:

对象通常在其接口声明中指定属性,这些属性属于以下几类之一:

  • 属性: 这些是简单的值,例如: 字符串或布尔值。

  • 一对一的关系: 具有自己属性的可变对象。对象的属性可以更改,而无需更改对象本身。例如: 一个pweson的名字可以更改, 但是person地址不会改变

  • 一对多关系: 集合对象。可以使用自定义集合类,但通常使用集合的实例NSArray或NSSet持有此类集合

访问非对象属性:

例如访问一个结构体, 因为结构体是不属于OC中的基本类型. 所以是无法直接用KVC进行访问的, 我们用NSValue来一层转换才可以这样使用.

ThreeFloats floats = {1., 2., 3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
//set
[person setValue:value forKey:@"threeFloats"];

//get
NSValue *value2 = [person valueForKey:@"threeFloats"];
ThreeFloats floats2;
[value2 getValue:&floats2];

三、集合运算符

集合运算符表现出三种基本的行为类型:

  • 聚合运算符: 以某种方式合并集合的对象,并返回通常与在右键路径中命名的属性的数据类型匹配的单个对象。该@count运营商是一个例外,它没有正确的关键路径并始终将返回一个NSNumber实例。

  • 数组运算符: 返回一个NSArray实例,该实例包含命名集合中保存的对象的某些子集。

  • 嵌套运算符: 处理包含其他集合的集合,并根据运算符返回一个NSArray或NSSet实例,该实例以某种方式组合嵌套集合的对象。

这里简单演示一下聚合运算符的使用.

聚合运算符包括:

  1. avg : 平均
  2. count : 个数
  3. max : 最大
  4. min : 最小
  5. sum : 和
NSMutableArray *personArr = [NSMutableArray array];
for (int i = 0; i < 10; i++) {
    Person *p = [Person new];
    p.age = @(10 + arc4random_uniform(10));
    [personArr addobject:p];
}

//平均age
float avg = [[personArr valueForKeyPath:@"@avg.age"] floatValue];

//age个数
int count = [[personArr valueForKeyPath:@"@count.age"] intValue];

//最大的age
int max = [[personArr valueForKeyPath:@"@max.age"] intValue];

//最小的age
int min = [[personArr valueForKeyPath:@"@min.age"] intValue];

//所有age和
int sum = [[personArr valueForKeyPath:@"@sum.age"] intValue];

四、KVC的底层原理

首先来看一下文档是怎么说明的.

例如: 对于Person类的name的成员变量(成员变量不会有setget方法 便于探究).

  1. 首先会去找是否有 set _set setIs方法直接进行访问
  2. 在accessInstanceVariablesDirectly 返回YES 情况下 依次找是否有 _name, _isName, name, isName
  3. 找不到则会异常

4.1 查找set相关方法

我们来用代码看一下这些情况:

@interface Person : NSObject {
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end

#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//MARK: - setKey. 的流程分析
- (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

我们进行四种情况进行调试:

  1. 存在所有方法

    只走了setName方法

  2. 不存在setName方法

    只走了_setName方法

  3. 不存在setName _setName方法

    只走了setIsName方法

  4. 不存在 setName _setName setIsName 方法 这个时候就会进入文档中的第二部, 看accessInstanceVariablesDirectly中返回的是YES还是NO, 如果为YES则会进行成员查找, 如果为NO则直接抛出异常

4.2 查找变量

当我们accessInstanceVariablesDirectly中返回的是YES的时候, 就会继续去查找相关变量来进行赋值.

Person *person = [[Person alloc] init];
[person setValue:@"Glen" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
NSLog(@"%@-%@",person->name,person->isName);
NSLog(@"%@",person->isName);

通过这段代码来分别查看一下赋值情况:

  1. _name _isName name isName存在

    赋值给了_name

  2. _isName name isName存在

    赋值给了_isName

  3. name isName存在

    赋值给了name

  4. isName存在

    赋值给了isName

  5. 没有变量

    直接抛出异常

4.3 KVO的get相关

官方文档:

  1. 判断是否有get is _方法, 假如找到直接返回结果, 找不到跳转到这里的第四步
  2. 找不到相关get方法, 先判断是否是NSArray, 是否是NSSet等集合类型.
  3. 如果为非集合类型, 则先看accessInstanceVariablesDirectly中返回的是YES还是NO, 如果为YES则去查找 _<key>, _is<Key>,<key>,或者is<Key>实例, 如果找到则直接返回, 找不到则进行下一步
  4. 细节处理,判断是否是对象指针 判断是否是NSNumber支持的标量类型, 是则转换NSNumber存储, 不是则转换NSValue存储
  5. 全部不符合则异常处理

大致原理与set相似, 多加了一些细节处理. 这里就不做演示.