iOS探索 -- KVC 原理分析

1,232 阅读16分钟

iOS 探索系列相关文章 :

iOS 探索 -- alloc、init 与 new 的分析

iOS 探索 -- 内存对齐原理分析

iOS 探索 -- isa 的初始化和指向分析

iOS 探索 -- 类的结构分析(一)

iOS 探索 -- 类的结构分析(二)

iOS 探索 -- 消息查找流程(一)

iOS 探索 -- 消息查找流程(二)

iOS 探索 -- 动态方法决议分析

iOS 探索 -- 消息转发流程分析

iOS 探索 -- 离屏渲染

iOS 探索 -- KVC 原理分析

KVC (Key-Value Coding), 是利用 NSKeyValueCoding 非正式协议实现的一种机制, 对象采用这种机制来提供对其属性的间接访问。当对象采用该协议时, 可以通过简洁统一的方法来访问其属性。简单来说, 就是我们在开发中可以通过key名直接访问对象的属性, 或者对属性进行赋值操作, 而不需要去调用明确的存取方法。这样就允许我们在运行时去动态地访问和修改对象的属性, 而不是在编译时决定。

KVC 简介

通过查看API我们不难发现, KVC的定义是通过对 NSObject 的扩展来实现的。所以对于所有集成了 NSObject 的类来说都可以使用 KVC, 也就是说出去少数类型 (结构体) 以外都可以使用KVC。下面是我们经常使用到的一些方法:

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

NSKeyValueCoding类别的其它方法:

// 默认为YES。 如果返回为YES,如果没有找到 set<Key> 方法的话, 会按照_key, _isKey, key, isKey的顺序搜索成员变量, 返回NO则不会搜索
+ (BOOL)accessInstanceVariablesDirectly;
// 键值验证, 可以通过该方法检验键值的正确性, 然后做出相应的处理
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 如果key不存在, 并且没有搜索到和key有关的字段, 会调用此方法, 默认抛出异常。两个方法分别对应 get 和 set 的情况
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// setValue方法传 nil 时调用的方法
// 注意文档说明: 当且仅当 NSNumber 和 NSValue 类型时才会调用此方法 
- (void)setNilValueForKey:(NSString *)key;
// 一组 key对应的value, 将其转成字典返回, 可用于将 Model 转成字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

KVC 原理分析

1. KVC 设值过程

当我们去调用 setValue:值 forKey:名字 设值方法时, 底层的执行机制大致如下:

  1. 程序会去优先调用, set<Key>: 或者 _set<Key>, setIs<Key> 方法, 如果存在这些命名规则的方法, 会直接调用该方法进行赋值。调用优先顺序按照上面书写的顺序。

    说明: 这里的 "key" 指成员变量名字, 书写格式需要符合 KVC 的命名规则。

  2. 如果没有找到步骤1的方法, 程序会回去判断 + (BOOL)accessInstanceVariablesDirectly; 方法的返回值, 如果该方法返回值为NO (默认为 YES, 在我们重写该方法时有可能返回NO, 一般不会返回NO), 则会执行 setValue: forUndefinedKey: 方法报错。

  3. 如果上一步方法的返回值为YES, 程序会去查找命名方式为 _<key>, _<isKey>, <key>, <isKey> 形式的实例变量, 加入存在该形式的实例变量, 则会直接将我们调用方法的值赋值给该实例变量。这里的查找优先顺序也会按照书写顺序去查找。

  4. 如果第三步没有查找到符合规则的实例变量, 程序就会去执行 setValue: forUndefinedKey: 方法进行报错。

下面是一段验证代码:

// 声明实例变量
@interface MCPerson : NSObject {
    @public
    NSString *_name;
    NSString *name;
    NSString *_isName;
    NSString *isName;
}
@end

// .m文件实现上面提到的方法进行监听
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (void)setValue:(id)value forUndefinedKey:(nonnull NSString *)key {
    NSLog(@"设置出现异常!!!");
}

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

// 测试代码
    MCPerson *person = [[MCPerson alloc] init];
    [person setValue:@"person" 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. 执行上面的代码, 查看打印信息如下:

可以看到程序执行了 setName 方法, 验证了上面步骤1的内容。而且我们发现三个方法中执行完第一个方法以后后面的就不会去执行了, 如果我们注释掉 setName 的话, 得到的结果是执行_setName , 由此就能得出方法的查找和执行优先顺序。

  1. 将上面的set方法注释掉, 然后 accessInstanceVariablesDirectly 方法的返回值改为 NO

根据打印信息发现调用了 setValue: forUndefinedKey: 方法抛出了异常, 下面再把返回值改为 YES。

可以发现我们的值被赋值给了 _name ,其余的三个实例变量仍然是空值, 然后试着把 _name注释掉就可以去一一验证实例变量的查找优先顺序了有兴趣的可以自己去验证一下。

2. KVC 取值过程

  1. 首先按照 get<Key>, <key>, is<Key>, _<key> 的顺序查找方法, 如果找到方法, 执行找到的方法得到返回值, 返回值的判断跳到第5步; 如果没有查找到方法, 进行下一步

  2. 如果没有找到上面的方法, KVC 就会去继续查找 countOf<Key>, objectIn<Key>AtIndex: (对应NSArray的方法), <key>AtIndexes: (对应NSArray 的 objectsAtIndexes: 方法) 格式的方法, 如果找到 countOf<Key> 和 另外两个方法中的一个, 就会返回一个可以响应所有NSArray方法的代理集合对象。当该代理集合对象接收到 NSArray 的方法调用时, 会去转换为对 countOf<Key>, objectIn<Key>AtIndex 或 <Key>AtIndexes 这几个方法的调用 (此外还有一个可选方法格式为 get<Key>:range )。(注意: 该类为NSKeyValueArray , 是NSArray的子类)

  3. 如果第2步仍然没有找到, 就会继续去查找 countOf<Key>, enumeratorOf<Key>, 和 memberOf<Key>: (对应NSSet的方法) 格式的方法, 如果这三种格式的方法都找到, 就会返回一个响应所有NSSet方法的代理集合对象, 反之则进行第4步。该集合对象会将接收到的NSSet方法调用转换为对 countOf<Key>, enumeratorOf<Key>, 和 memberOf<Key>: 方法的调用。

  4. 如果没有找到任何符合要求的方法, 然后 accessInstanceVariablesDirectly 的返回值为YES, 会像上面的设值过程一样去查询实例变量 _<key>, _<isKey>, <key>, , 如果查询到符合条件的实例变量, 会直接取出实例变量的值, 然后进行第5步。反之, 直接到第6步。

  5. 如果第4步获取到的属性值是一个对象指针, 直接返回结果; 如果该值是 NSNumber 支持的标量类型, 将其存储为 NSNumber类型的实例然后返回; 如果该值不是 NSNumber 支持的标量类型, 将其转换为 NSValue对象然后返回。

  6. 调用 valueForUndefinedKey: 方法进行报错。

下面是关于取值过程的代码验证:

// 
@property (nonatomic,strong) NSArray *array;
@property (nonatomic,strong) NSSet *set;

@property (nonatomic,strong) NSMutableString *arrayM;
@property (nonatomic,strong) NSMutableSet *setM;
@property (nonatomic,strong) NSMutableOrderedSet *orderSetM;

//
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"取值出现异常!!!");
    return key;
}

//
- (NSString *)getName {
    return NSStringFromSelector(_cmd);
}
- (NSString *)name {
    return NSStringFromSelector(_cmd);
}
- (NSString *)isName {
    return NSStringFromSelector(_cmd);
}
- (NSString *)_name {
    return NSStringFromSelector(_cmd);
}

#pragma - NSArray -
- (NSUInteger)countOfPens {
    NSLog(@"- %s -", __func__);
    return [self.array count];
}
- (id)objectInPensAtIndex:(NSUInteger)index {
    NSLog(@"- %s -", __func__);
    return self.array[index];
}

#pragma - NSSet -
- (NSUInteger)countOfBooks {
    NSLog(@"- %s -", __func__);
    return [self.set count];
}
- (NSEnumerator *)enumeratorOfBooks {
    NSLog(@"- %s -", __func__);
    return [self.set objectEnumerator];
}
- (NSString *)memberOfBooks:(NSString *)object {
    NSLog(@"- %s -", __func__);
    return [self.set containsObject:object] ? object : nil;
}

// 验证代码
    person->_name = @"我是 _name";
    person->name = @"我是 name";
    person->isName = @"我是 isName";
    person->_isName = @"我是 _isName";
    
    NSLog(@"***** %@", [person valueForKey:@"name"]);
    
    person.array = @[@"pen0", @"pen1", @"pen2", @"pen3", @"pen4"];
    NSArray *array = [person valueForKey:@"pens"];
    NSLog(@"%@", [array objectAtIndex:1]);
    NSLog(@"数量 %ld", [array count]);
    NSLog(@"是否存在该值 %d", [array containsObject:@"pen2"]);

    person.set = [NSSet setWithArray:person.array];
    NSSet *set = [person valueForKey:@"books"];
    [set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop){
        NSLog(@"遍历set: %@", obj);
    }];
    NSLog(@"是否存在该值 %d", [set containsObject:@"pen2"]);

打印结果: 通过上面的代码可以去检索KVC在 valueForKey:@"name" 是的查询机制, 这里不再一一列出, 有兴趣的可以自己去一一验证。

3. KVC 与 容器类

我们知道对象的属性既可以是一对一的, 也可以是一对多的。可以是有序的数组, 也可以是无序的集合。当对象属性是可变容器时, 苹果给我们提供了下面的方法:

1. 有序的

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

该方法返回一个可变的有序数组, 当调用该方法时KVC的搜索顺序是这样的:

  • 首先搜索 insertObject:in<Key>AtIndex: 和 removeObjectFrom<Key>AtIndex: 方法 (对应NSMutableArrayinsertObject:atIndex: 和 removeObjectAtIndex: 方法), 或者insert<Key>:atIndexes: 和 remove<Key>AtIndexes: (对应NSMutableArrayinsertObjects:atIndexes: 和 removeObjectsAtIndexes: 方法)。如果至少找到一个 insert方法和一个remove方法,就会返回一个可以相应NSMutableArray所有方法的代理集合, 当该代理集合对象接收到 NSMutableArray 的方法调用时, 会去转换为对 insertObject:inAtIndex: , removeObjectFromAtIndex: 或者 insertAdIndexes , removeAtIndexes组合的形式调用 (此外还有两个可选的方法replaceOnjectAtIndex:withObject:, replaceAtIndexes:with:) 。

  • 如果上面的条件没有成立, 会继续搜索格式为 set<Key> 的方法, 如果找到, 那么代理集合对象接收到的NSMutableArray方法调用都会去调用set<Key>方法。就是说, 取出的代理集合如果被修改后, 都会通过调用 set<Key> 方法重新赋值回去, 这样做的话大大降低了效率。(所以尽量实现上面的方法)

  • 如果上一步的方法还是没有找到, 会去检查 accessInstanceVariablesDirectly 的返回值, 为YES, 会按照 _<key>, <key> 的顺序去搜索实例变量, 如果找到的话, 代理集合接收到的NSMutableArray消息直接交给这个实例变量处理。

  • 如果仍然没有结果, 调用 valueForUndefinedKey:

  • 关于 mutableArrayValueForKey 的用法, 网上只找到了关于NSMutableArray 添加观察者时。如果对象的属性是 NSMutableArray、NSMutableSet、NSMutableDictionary 等集合类型时, 如果我们对其添加KVO, 会发现当添加或者移除元素时并不能接收到变化。因为KVO是在系统检测到某个属性的内存地址或者常量发生改变时, 才会去发送通知。一种方法是我们去手动调用方法通知对象的改变, 但是并不推荐, 因为我们无法像系统一样准确的知道其改变。另外一种就是利用 mutableArrayValueForKey :

@interface MCStudent ()
@property (nonatomic,strong) NSMutableArray *arr;
@end
  
- (instancetype)init {
    if (self = [super init]) {
        _arr = [NSMutableArray array];
        [self addObserver:self forKeyPath:@"arr" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    }
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"change: %@", change);
}
- (void)dealloc {
    [self removeObserver:self forKeyPath:@"arr"];
}
- (void)addItem{
    [_arr addObject:@"1"];
}
- (void)addItemObserver{
    [[self mutableArrayValueForKey:@"arr"] addObject:@"1"];
}
- (void)removeItemObserver{
    [[self mutableArrayValueForKey:@"arr"] removeLastObject];
}
// 测试代码
    [student addItem];
    [student addItemObserver];
    [student removeItemObserver];
// 打印结果如下
2020-02-16 16:16:21.315056+0800 002-KVC取值&赋值过程[2281:1547198] change: {
    indexes = "<_NSCachedIndexSet: 0x60000107edc0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        1
    );
}
2020-02-16 16:16:21.315270+0800 002-KVC取值&赋值过程[2281:1547198] change: {
    indexes = "<_NSCachedIndexSet: 0x60000107edc0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 3;
    old =     (
        1
    );
}

从上面的代码可以看出, 当我们按照通常的方式去给数组添加对象时, 并没有触发KVO的监听方法, 只有通过 mutableArrayValueForKey 方法拿到数组然后再去进行添加/移除时才会触发KVO。

2. 无序的

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

该方法返回一个可变的无序数组, KVC搜索顺序如下:

  • 首先会去查询 addObject<Key>Object: , remove<Key>Object: 或者 add<Key>: , remove<Key>: , 如果至少找到一个 insert和一个remove方法, 就会返回一个可以响应NSMutableSet所有方法的代理集合, 当该代理集合对象接收到 NSMutableSet 的方法调用时, 会去转换为对 addObject<Key>Object: , remove<Key>Object: 或者 add<Key>:, remove<Key>:组合的形式调用 (此外还有两个可选的方法replaceOnjectAtIndex:withObject:, replaceAtIndexes:with:) 。
  • 如果 receiver 是 managed object, 那么就不会继续搜索 (See Managed Object Accessor Methods in Core Data Programming Guide for more information) 。
  • 如果上一步的方法没有找到,则搜索set<Key>: 格式的方法,如果找到,那么发送给代理集合的 NSMutableSet 方法最终都会调用set<Key>:方法。 也就是说,mutableSetValueForKey 取出的代理集合修改后,用set<Key>: 重新赋值回去。这样做效率会低很多。所以推荐实现上面的方法。
  • 如果上一步的方法还是没有找到, 会去检查 accessInstanceVariablesDirectly 的返回值, 为YES, 会按照 _<key>, <key> 的顺序去搜索实例变量, 如果找到的话, 代理集合接收到的NSMutableSet 消息直接交给这个实例变量处理。
  • 如果仍然没有结果, 调用 valueForUndefinedKey:

4. KVC 与 字典

当对字典对象使用KVC时, KVC给我们提供了下面两个方法:

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

dictionaryWithValuesForKeys: 该方法接收一组key, 然后将该组key对应的属性封装成一个字典返回。

setValuesForKeysWithDictionary: 通过一个字典来修改对应key的值。

KVC 的使用注意事项

1. 在KVC中使用 keyPath

在实际过程中, 一个类的成员变量有可能是自定义的类或者其他的复杂类型, 这时候如果想要使用KVC获取到自定义类的属性就会比较麻烦。这时KVC给我们提供了一个解决方案, 键路径 keyPath。方法如下:

- (nullable id)valueForKeyPath:(NSString *)keyPath;  //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值
// 
MCStudent *student = [[MCStudent alloc] init];
student.classNumber = @"num1";
student.number = @"007";
person.student = student;
NSLog(@"classNum: %@", [personvalueForKeyPath:@"student.classNumber"]);
// 打印结果
2020-02-16 13:27:08.482381+0800 002-KVC取值&赋值过程[96366:1435734] classNum: num1

上面展示keyPath的简单用法, 此时如果我们调用的方法是 valueForKey: 的话, 一般情况下系统会去调用undefinedKey方法, 因为没有找到这个属性及其相关的方法和实例变量。KVC在此方法中的搜索机制首先根据 " . " 来分割key, 然后在去按照上面的顺序去搜索下去。

2. KVC 的自动转换

我们进行设值或者取值的时候, 不是每次都是对象类型, 但是在 valueForKey: 和 setValue: forKey: 时总是需要或者返回一个id对象类型。看一下下面的代码:

typedef struct {
    float x, y, z;
} ThreeFloats;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, assign) int  age;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic) ThreeFloats  threeFloats;
//
  [person setValue:@18 forKey:@"age"];
// 上面那个表达 大家应该都会! 但是下面这样操作可以?
  [person setValue:@"20" forKey:@"age"]; // int - string
  NSLog(@"%@-%@",[person valueForKey:@"age"],[[person valueForKey:@"age"] class]);//__NSCFNumber
  [person setValue:@"20" forKey:@"sex"];
  NSLog(@"%@-%@",[person valueForKey:@"sex"],[[person valueForKey:@"sex"] class]);//__NSCFNumber

// 打印结果
2020-02-16 14:40:31.263662+0800 004-KVC异常小技巧[98873:1483589] 20-__NSCFNumber
2020-02-16 14:40:33.829650+0800 004-KVC异常小技巧[98873:1483589] 1-__NSCFBoolean

如果原本的变量类型是值类型或者布尔类型, 我们直接以字符串进行赋值以后, 得出的结果为 __NSCFNumber 和__NSCFBoolean 类型, 说明在我们进行setValue 和 getValue操作时, 系统帮我们进行了自动转换。那么当变量类型是结构体类型的时候呢, 此时我们是无法再去直接使用字符串赋值的, 需要我们将其转换为 NSValue类型:

    // 赋值操作
    ThreeFloats floats = {1., 2., 3.};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSValue *reslut = [person valueForKey:@"threeFloats"];
    NSLog(@"%@",reslut);
    NSLog(@"%@-%@",[person valueForKey:@"threeFloats"],[[person valueForKey:@"threeFloats"] class]);//NSConcreteValue

    // 取值操作
    ThreeFloats th;
    [reslut getValue:&th] ;
    NSLog(@"%f - %f - %f",th.x,th.y,th.z);

    // 打印结果
    2020-02-16 15:05:00.494832+0800 004-KVC异常小技巧[99701:1501772] {length = 12, bytes = 0x0000803f0000004000004040}-NSConcreteValue

通过打印结果可以看出, 转换成 NSValue 类型以后赋值成功了, 然后如果我们需要使用 valueForKey 取值时, 同样取出的也是 NSValue 类型的值, 此时需要进行如上面样将其转换为结构体然后进行使用。所以当我们进行KVC设值和取值操作时, 因为我们传递进去和取出来的都是id类型的值, 有时候需要我们自己去保证类型的正确性。

3. KVC 异常处理

这一点需要结合上面的第2点进行说明, 我们知道KVC有时候会帮我们去自动转换我们所传的值, 但是当我们传 nil 的时候KVC是怎么处理的呢。下面看一段代码:

- (void)setNilValueForKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key{
    NSLog(@"你傻不傻: 设置 %@ 是空值",key);
}
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, assign) int  age;
@property (nonatomic, assign) BOOL sex;
@property (nonatomic) ThreeFloats  threeFloats;

// 测试代码
  NSLog(@"******2: 设置空值******");
  [person setValue:nil forKey:@"age"]; 
  [person setValue:nil forKey:@"subject"];
  [person setValue:nil forKey:@"sex"];
  [person setValue:nil forKey:@"threeFloats"];
// 打印结果
2020-02-16 16:58:44.538117+0800 004-KVC异常小技巧[3703:1577245] ******2: 设置空值******
2020-02-16 16:58:44.538200+0800 004-KVC异常小技巧[3703:1577245] 你傻不傻: 设置 age 是空值
2020-02-16 16:58:44.538293+0800 004-KVC异常小技巧[3703:1577245] 你傻不傻: 设置 sex 是空值
2020-02-16 16:58:44.538487+0800 004-KVC异常小技巧[3703:1577245] 你傻不傻: 设置 threeFloats 是空值

可以看出给subject设置空值时, 不会走该方法, 其余三个进行 nil 赋值时都会调用该方法抛出异常。官方文档上面说明的是针对需要转换为 NSNumber 和 NSValue 类型的数据赋值 nil 时会抛出异常, 所以 当我们给 NSString 类型的subject进行赋值时并没有报错。

4. KVC 正确性验证

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

该方法默认返回YES, 如果类中实现了该方法, 那么就会去调用新实现的方法来返回。

- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing  _Nullable *)outError{
    if([inKey isEqualToString:@"name"]){
        [self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:inKey];
        return YES;
    }
    *outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:10088 userInfo:nil];
    return NO;
}

如上所示, 我们可以在该方法里面去验证某个key是否允许通过KVC去设定, 然后进行相关处理和返回。(注意: KVC是不会去主动验证的, 需要我们在该方法里面去实现验证。)

总结

本文主要是KVC的一些原理和一些需要注意的点, 所有的东西都是通过阅读官方的文档和自己结合官方文档的一些理解。希望读者在看完后能对KVC有更进一步的理解, 同时推荐你们去看一下官方文档上的详细说明。如果有不对的地方或者不合理的地方欢迎大佬们提出, 我们一起学习, 一起进步。