iOS大师养成之路—手撕KVC

329 阅读29分钟

1.前言

在聊KVC和KVO之前我们来抛出几个问题

  • KVC 和 KVO 是啥关系?有什么区别?
  • 我们平时对属性赋值的时候直接 A.xx = xxx;就可以了。我们在前面的篇章中已经聊到了类的加载过程,属性是如何加载到内存中的。那么这个A.xx = xxx 的过程是怎么实现的对类的属性赋值的呢?
  • KVC 是什么?为什么会设计这个机制?有何优缺点?如果有缺点那么又是如何进行容错保护的呢?
  • KVO 是什么?
  • 限于篇幅原因,KVO不便展开分析,我放下一篇了

2. KVC 介绍

KVC 全称为 Key-Value coding 什么意思呢?就是键值编码,是由NSKeyValueCoding非正式协议启用的一种机制,对象采用该协议来提供对其属性的间接访问。当对象符合键值编码规则的时候。其属性可以通过简洁、统一的消息传递接口通过字符串参数进行寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。 这是苹果官网文档介绍的,其中讲到了一下几个点

  • 本质是键值编码,是NSKeyValueCoding启用的一种机制
  • 使用方式是通过字符串参数进行寻址
  • 是对固定的方法访问器的一种补充 可能看到上面几点介绍还有点懵逼,没关系,我们打开NSKeyValueCoding看看到底是怎么定义的

Screen Shot 2022-02-25 at 11.16.02 AM.png 由上图可知道,NSKeyValueCoding 是NSObject的一个分类,只不过命名跟我们普通的方式不太一样,实质上是对NSObject的一种能力扩充。NSKeyValueCoding 定义的方法就是对各类对象的属性方法。

我们来看看都有些啥?


@property (class, readonly) BOOL accessInstanceVariablesDirectly;

- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setNilValueForKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

看着这个分类的东西其实一点都不多。但是你真的都对其中的用法都熟练掌握了吗?

3. KVC - valueForKey

我们先来看看取值的过程,为了保证整个过程可靠性和权威性,我特意翻阅了苹果的官方文档。各位读者有兴趣的话可以自己去仔细看看 苹果官网对KVC的说明 或者跟随我一起简单明了地探索验证官网所说的每个步骤

3.1. 基本类型的取值过程

假定 key 就是我们要找的值

  • 第 1 步 按顺序查找get<Key><key>is<Key>, or _<key>,方法,如果找到直接跳到第 5 步,否则继续往下执行。
  • 第 2 步 此步骤是为了可能是数组类型 countOf<Key>(必须实现)  objectIn<Key>AtIndex:and <key>AtIndexes:(两个实现一个即可以)
  • 第 3 步 此步骤是为了能兼容可能是集合类型的情况, countOf<Key>enumeratorOf<Key>, and memberOf<Key>:这三个方法必须都实现才满足。
  • 第 4 步 查看是否重写了method accessInstanceVariablesDirectly 同时returns YES 按照这个顺序查找是否有这些实例变量 _<key>_is<Key><key>, or is<Key>, 如果找到,到第5步,否则直接跳转到第6步。
  • 第 5 步 如果检索到的属性值是对象指针,只需返回结果,如果该值是NSNumber支持的标量类型,请将其存储在NSNumber实例中并返回。如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。
  • 第 6 步 如果以上所有的步骤都失败了,会调用valueForUndefinedKey:。默认情况下,这会引发异常,如果想避免异常中断需要该类重写valueForUndefinedKey:并做相关的可能的措施。

3.2. 取值过程的验证

光说不练是假把式,光看不实践同样是假把式。不自己亲自试试你就会是那个一看就会一试就废的人。为什么呢?因为总有些坑,你趟过去了就看到不一样的风景。不趟过下次就把自己埋里面了😄

3.2.1. 取值过程第一步验证

先做点准备工作,我准备了一个LCPerson 类,定义了一个 name 的属性,为了避免系统自动给属性添加set和get方法干扰整个过程 我用@dynamic name;限制了一下。

@interface LCPerson : NSObject {
    NSString *_name;
    @public
    NSString *myName;
}
@property (nonatomic, copy)     NSString    *name;
@end

手动实现set方法,并按照第一步描述的实现下列方法

#import "LCPerson.h"

@implementation LCPerson

@dynamic name;

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

/********************  取值步骤第一步 1.方法查找    ********************/

- (NSString *)getName {
    NSLog(@"%@ %s %@",NSStringFromClass([self class]) ,__func__, _name);
    return _name;
}

- (NSString *)name {
    NSLog(@"%@ %s %@",NSStringFromClass([self class]) ,__func__, _name);
    return _name;
}

- (NSString *)isName {
    NSLog(@"%@ %s %@",NSStringFromClass([self class]) ,__func__, _name);
    return _name;
}

- (NSString *)_name {
    NSLog(@"%@ %s %@",NSStringFromClass([self class]) ,__func__, _name);
    return _name;
}

- (void)setName:(NSString *)name {
    _name = name;
}

@end

然后在VC 中这么调用, 从上到下依次注释上面的方法,看打印结果

LCPerson *person    = LCPerson.new;
[person setValue:@"JC" forKey:@"name"];
NSLog(@"%@",[person valueForKey:@"name"]);

Screen Shot 2022-03-08 at 3.01.09 PM.png

Screen Shot 2022-03-08 at 3.02.56 PM.png

Screen Shot 2022-03-08 at 3.03.41 PM.png

Screen Shot 2022-03-08 at 3.04.30 PM.png 从上面的打印结果发现确实是按照步骤一的方法顺序调用的,第一步得以验证。我们确实看到了平时都不关注的东西,居然还有这么多的get方法样式!!

3.2.2 取值过程第二步的验证

还是用刚刚的 LCPerson 类,只不过把刚刚的get方法全部注释掉,然后实现第二部描述的方法。为了能配合这些方法实现我新加了 array 数组属性在调用 name 的KVC取值方法之前初始化一下

@interface LCPerson : NSObject {
    NSString *_name;
    @public
    NSString *myName;
}
@property (nonatomic, copy)     NSString    *name;
@property (nonatomic, strong)   NSArray     *array;
@end
#import "LCPerson.h"

@implementation LCPerson

@dynamic name;

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

/********************  取值步骤第二步 数组方法查找    ********************/
- (NSUInteger)countOfName {
    return self.array.count;
}

- (id)objectInNameAtIndex:(NSUInteger)index {
    return [self.array objectAtIndex:index];
}

@end

在VC 中进行如下验证,很奇怪的事情来了,我把第一步中描述的简单的get方法全部注释掉了,这时候我还是调用valueForKey的方法既然没有崩溃,没崩溃就算了还整出来一个这么奇怪的事情 --> 我定义一个字符串结果给我整出来了一个数组 !!

Screen Shot 2022-03-08 at 3.21.03 PM.png 换一个组合 换成countOfName 和 nameAtIndexes

#import "LCPerson.h"

@implementation LCPerson

@dynamic name;

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

/********************  setter步骤第二步 数组方法查找    ********************/
- (NSUInteger)countOfName {
    return self.array.count;
}

- (NSArray *)nameAtIndexes:(NSIndexSet *)indexes {
   return [self.array objectsAtIndexes:indexes];
}
@end

Screen Shot 2022-03-08 at 3.28.29 PM.png 还是👌的,结果和上一次运行的一样。第二步得以验证。

3.2.3 取值过程第三步的验证

还是用第二步中的代码,只不过把数组的方法改成集合的,然后定义一个集合 set 属性,并在调用 name 的取值方法之前进行一个简单的赋值

@interface LCPerson : NSObject {
    NSString *_name;
    @public
    NSString *myName;
}
@property (nonatomic, copy)     NSString    *name;
@property (nonatomic, strong)   NSSet       *set;
@end
#import "LCPerson.h"

@implementation LCPerson

@dynamic name;

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

/********************  取值步骤第三步 集合方法查找    ********************/

- (NSUInteger)countOfName {
    return self.set.count;
}

- (id)memberOfName:(id)object {
    return [self.set member:object];
}

- (NSEnumerator *)enumeratorOfName {
    return self.set.objectEnumerator;
}
@end

Screen Shot 2022-03-08 at 3.42.56 PM.png 通过打印的结果我们发现,直接把 name 当成集合处理了返回的是 person 对象的 set第三部描述的步骤得以实践验证。

奇怪是挺奇怪的谁叫我们实现了这些方法呢?

3.2.4 取值过程第四步的验证

这里我们新增几个实例变量NSString *_isName; NSString *name;NSString *isName;,同时在set方法中给不同的实例变量赋值不同的值来区分到底取的是哪个实例变量。set方法变量做了改动避免和实例变量重名。

@interface LCPerson : NSObject {
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@property (nonatomic, copy)     NSString    *name;
@end
#import "LCPerson.h"

@implementation LCPerson

@dynamic name;

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

/********************  取值步骤第四步 实例变量查找    ********************/

- (void)setName:(NSString *)tname {
    _name = [NSString stringWithFormat:@"%@ _name",tname];
    _isName = [NSString stringWithFormat:@"%@ _isName",tname];
    name = [NSString stringWithFormat:@"%@ _name",tname];
    isName = [NSString stringWithFormat:@"%@ _isName",tname];
}
@end

全部打开取的是 _name Screen Shot 2022-03-08 at 4.19.02 PM.png 注释掉 _name 取的是 _isName Screen Shot 2022-03-08 at 4.21.01 PM.png 注释掉 _name ,_isName 取的是 name Screen Shot 2022-03-08 at 4.22.24 PM.png 注释掉 _name ,_isName,name 取的是 isName Screen Shot 2022-03-08 at 4.24.11 PM.png 以上打印结果验证了 第四步的描述步骤是完全OK的 ,但是有个前提就是 accessInstanceVariablesDirectly必须重写而且是返回为 YES

3.2.5 取值过程第五步的验证

新增加一个 int 类型的属性 blood 和自定义结构体 Power 属性 power 在VC中打印查看

Screen Shot 2022-03-08 at 5.11.43 PM.png 如上打印结果 以及 lldb 指令调试 显示,对象类型如 name 就是直接返回,而对于标量类型就是包装成 NSNumber 返回如 blood 属性 如果是其他类型则会返回 NSValue 类型如 power 属性 OK 第五步验证跟文档描述是一致的

3.2.6 取值过程第六步的验证

这里还是以 name 属性为例,把第四步中例举的实例变量全部注释掉,在set方法中换一个实例变量进行赋值。

@interface LCPerson : NSObject {
    //NSString *_name;
    @public
    NSString *myName;
}
@property (nonatomic, copy)     NSString    *name;
@property (nonatomic, strong)   NSArray     *array;
@end
#import "LCPerson.h"

@implementation LCPerson

@dynamic name;

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (void)setName:(NSString *)name {
    myName = name;
}

/********************  取值步骤第六步 异常回调    ********************/

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"兄得  没有这个key: %@",key);
    return nil;
}

@end

Screen Shot 2022-03-08 at 5.50.48 PM.png

在经历了1-5步之后,走到了这个异常回调点 valueForUndefinedKey 由于我重写了这个方法所以就没有直接崩溃。OK 第六步得以实践验证 过程看似顺风顺水没有任何问题,但就仅限于此吗是不是干货太少了吧?

3.2.7 Getter取值过程的反思

经历以上六个取值的步骤之后我有以下反思,不知道各位读者有没有共鸣?

给与容错的机会越多越可能出现一些看上去莫名其妙的事情,比如我让name 第1步骤不满足 然后我定义一个isName的属性,我给name 赋值 JC 和 isName 赋值 JasonPang 再打印name的值,各位读者决定会打印出name的值吗?

答案是 JasonPang 是的,他打印的是isName的值 为什么?虽然我们没有给name实现简单的get方法,但是系统自动给isName生成了,而且名字也是isName.

系统自动给属性生成的get方法是 key 而最先查找的确认 getKey 看上去就像一个漏洞。为啥这么说?如果哪天不知道谁整了个 getKey 属性是不是就把 key 属性屏蔽了?这里也是一个坑点,就是可能我们用valueForKey取到的不是我们想取的那个值 看到这里各位读者是不是有点惊讶😱

第二步和第三步看上去很诡异,事实上通过KVC可以取到一个不存在的属性,只要这个类实现了对应的数组和集合的方法他就能返回一个数组或者集合给你。有什么用呢?其实反思苹果提供这个KVC机制就是一种间接访问实例变量的路径。我个人感觉用作调试是很棒的体验,我不需要对外暴露属性对代码的改动和影响是最小的。

异常拦截,通过字符串进行传参那可能触发的异常概率就大大增加了,重写 valueForUndefinedKey 之后我们在这个方法里面能做的事情可就多了,我们能进行异常上报,收集到底是哪个类的哪个属性的KVC取值方法异常。在上线前前统一检查一波是不是避免了可能的异常闪退?甚至我们可以针对一些重要的属性进行回传对应的值?

所以,通过KVC取值的话还是要谨慎避免可能的问题,毕竟只是作为一种扩展机制,广泛使用的话既有潜在的风险也有维护的成本

3.3. 基本类型的设值过程

  • 第 1 步,按照此顺序查找set方法 set<Key>: 或者 _set<Key> 找到直接赋值,并结束。如果没找到则走第 2 步
  • 第 2 步,查看 accessInstanceVariablesDirectly 返回值是否为 YES, 默认是YES,如果重写了的话需要关注返回值。如果返回YES 则按照此顺序查找相关实例变量 _<key>_is<Key><key>, or is<Key>,如果找到对应的实例变量则直接给该实例变量赋值并结束流程。如果返回是NO,则走第 3 步
  • 第 3 步,回调 setValue:forUndefinedKey: 方法,并抛出异常。

3.4. 基本类型的设值过程验证

3.4.1 设值方法查找

还是使用之前的代码,只是在LCPerson类中实现两个设值方法,在VC中调用 person setValue:@"JasonPang" forKey:@"name"];

@interface LCPerson : NSObject {
}
@property (nonatomic, copy)     NSString    *name;

@end
#import "LCPerson.h"

@implementation LCPerson

@dynamic name;

/********************  设值步骤第一步 设值方法查找    ********************/

- (void)setName:(NSString *)name {
    NSLog(@"%@ %s %@",NSStringFromClass([self class]) ,__func__, name);
}

- (void)_setName:(NSString *)name {
    NSLog(@"%@ %s %@",NSStringFromClass([self class]) ,__func__, name);
}
@end

两个方法都打开的情况 Screen Shot 2022-03-10 at 12.37.50 PM.png

关闭第一个方法的情况 Screen Shot 2022-03-10 at 12.38.17 PM.png

我们看到即使我删除了所有的实例变量,并没有报错。对 name 进行KVC设值的过程中只是找到设值方法,并且是按照顺序查找,找着之后进行赋值。并不关心实例变量是否存在。

3.4.2 实例变量查找

还是使用之前的代码,只是在LCPerson类中实现两个设值方法,在VC中调用 person setValue:@"JasonPang" forKey:@"name"]; 打印所有的实例变量并逐一注释,看是否是按顺序赋值

@interface LCPerson : NSObject {
@public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@property (nonatomic, copy)     NSString    *name;

@end
#import "LCPerson.h"

@implementation LCPerson

@dynamic name;

/********************  设值步骤第二步 实例变量查找    ********************/
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

@end

全开时打印的是_name Screen Shot 2022-03-10 at 2.08.22 PM.png

注释掉_name,打印有值的是 _isName Screen Shot 2022-03-10 at 2.11.50 PM.png

注释掉_name 、_isName,打印有值的是 name Screen Shot 2022-03-10 at 2.14.15 PM.png

注释掉_name 、_isName、name 打印有值的是 isName Screen Shot 2022-03-10 at 2.21.50 PM.png

从上面的打印结果看,即使没有实现任何设值方法也没有报错,赋值都是按照_name 、_isName、name 、isName 顺序来打印

3.4.3 异常回调

把所有的实例变量都删除,然后为了看到异常回调确实走了,我们把 setValue:forUndefinedKey: 重写一下。再跑一下 name 的KVC赋值方法

@interface LCPerson : NSObject {
}
@property (nonatomic, copy)     NSString    *name;

@end
#import "LCPerson.h"

@implementation LCPerson

@dynamic name;

/********************  设值步骤第三步 异常回调    ********************/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {

    NSLog(@"兄嘚 没有这个  %@ 的set方法  value: %@",key, value);

}

@end

Screen Shot 2022-03-10 at 2.43.41 PM.png

3.5. 可变数组的设值过程

对于可变数组kvc的默认实现是 mutableArrayValueForKey: 通过一个参数关键字key进行访问,返回接收访问器调用的对象名为key的可变的代理数组,通过以下步骤来接收访问器调用消息。

第一步,先查询有没有下面的成对方法出现(所谓成对就是至少有一个插入和一个移除的方法),后面的替换方法也可以实现这样的话给可以给代理访问提供最佳的性能

insertObject:in<Key>AtIndex:  removeObjectFrom<Key>AtIndex:
insert<Key>:atIndexes:        remove<Key>AtIndexes:

以上方法等同于 NSMutableArray的如下方法
insertObject:atIndex:          removeObjectAtIndex:
insertObjects:atIndexes:       removeObjectsAtIndexes:

replaceObjectIn<Key>AtIndex:withObject:  replace<Key>AtIndexes:with<Key>: //可选实现的替换方法

第二步,如果没有实现第一步中的可变数组访问方法,那么返回的代理对象通过给原有的接收 mutableArrayValueForKey:的对象发送 setKey: 消息来响应 NSMutableArray 的消息

官方温馨提示:虽然会有这一步可以走,但是此步骤中描述的机制比上一步的机制效率低得多,
因为它可能涉及反复创建新的集合对象,而不是修改现有的集合对象。
因此,在设计自己的键值编码兼容对象时,您通常应该避免使用它

第三步,如果既没有找到可变数组的相关方法也没有响应的访问器同时accessInstanceVariablesDirectly返回的是YES,默认是YES,那么接下来就会按照顺序查找是不是有 _key key 的实例变量,如果找到了就应返回一个代理对象将每一次可变数组的消息都转发给该实例变量。所以通常这些实例变量应该是可变数组或者可变数组的子类。 什么意思呢,就是说我找到了一个可能是你想要的可变数组类型,同时还返回一个代理对象做消息转发,你发的任何关于可变数组的消息其实就是给我找到的这个实例变量发。

第四步,如果以上3步都失败了,那么就会给原来接收 mutableArrayValueForKey 消息的对象发送setValue:forUndefinedKey消息,此流程等同于普通的 setValue:forKey:流程

3.5.1. 可变数据是的设值过程Demo实践取证过程

我还是在LCPerson中先定义一个不可变数组属性flruits,然后在viewcontroller中通过kvc的方式取出一个可变数组。我们先按照第一步中的介绍,至少实现一个插入和删除方法。我们有以下4中组合,且都只发送一个插入消息看打印结果

@interface LCPerson : NSObject 
@property (nonatomic, strong)   NSArray *flruits;
@end


@implementation LCPerson
- (instancetype)init
{
    self = [super init];
    if (self) {
        _flruits = @[@"Apple"];
    }
    return self;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (void)insertObject:(id)object inFlruitsAtIndex:(NSUInteger)index {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

- (void)removeObjectFromFlruitsAtIndex:(NSUInteger)index 
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

/********************  异常回调    ********************/

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"兄得  没有这个key: %@",key);
    return nil;
}

/******************** 异常回调    ********************/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"兄嘚 没有这个  %@ 的set方法  value: %@",key, value);
}

@end

//在VC中这样调用测试
- (void)mutableArrayPatternStudy {
    LCPerson *person    = LCPerson.new;
    NSMutableArray *flruits = [person mutableArrayValueForKey:@"flruits"];
    [flruits addObject:@"banana"];
    NSLog(@"%@",flruits);
}

看打印结果 Screen Shot 2022-06-20 at 11.26.36.png

@interface LCPerson : NSObject 
@property (nonatomic, strong)   NSArray *flruits;
@end


@implementation LCPerson
- (instancetype)init
{
    self = [super init];
    if (self) {
        _flruits = @[@"Apple"];
    }
    return self;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (void)insertFlruits:(NSArray *)array atIndexes:(NSIndexSet *)indexes {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

- (void)removeFlruitsAtIndexes:(NSIndexSet *)indexes {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

/********************  异常回调    ********************/

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"兄得  没有这个key: %@",key);
    return nil;
}

/******************** 异常回调    ********************/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"兄嘚 没有这个  %@ 的set方法  value: %@",key, value);
}

@end

//在VC中这样调用测试
- (void)mutableArrayPatternStudy {
    LCPerson *person    = LCPerson.new;
    NSMutableArray *flruits = [person mutableArrayValueForKey:@"flruits"];
    [flruits addObject:@"banana"];
    NSLog(@"%@",flruits);
}

Screen Shot 2022-06-20 at 11.28.59.png

@interface LCPerson : NSObject 
@property (nonatomic, strong)   NSArray *flruits;
@end


@implementation LCPerson
- (instancetype)init
{
    self = [super init];
    if (self) {
        _flruits = @[@"Apple"];
    }
    return self;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (void)insertObject:(id)object inFlruitsAtIndex:(NSUInteger)index {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

- (void)removeFlruitsAtIndexes:(NSIndexSet *)indexes {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

/********************  异常回调    ********************/

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"兄得  没有这个key: %@",key);
    return nil;
}

/******************** 异常回调    ********************/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"兄嘚 没有这个  %@ 的set方法  value: %@",key, value);
}

@end

//在VC中这样调用测试
- (void)mutableArrayPatternStudy {
    LCPerson *person    = LCPerson.new;
    NSMutableArray *flruits = [person mutableArrayValueForKey:@"flruits"];
    [flruits addObject:@"banana"];
    NSLog(@"%@",flruits);
}

Screen Shot 2022-06-20 at 11.30.29.png

@interface LCPerson : NSObject 
@property (nonatomic, strong)   NSArray *flruits;
@end


@implementation LCPerson
- (instancetype)init
{
    self = [super init];
    if (self) {
        _flruits = @[@"Apple"];
    }
    return self;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}


- (void)removeObjectFromFlruitsAtIndex:(NSUInteger)index 
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

- (void)insertFlruits:(NSArray *)array atIndexes:(NSIndexSet *)indexes {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

/********************  异常回调    ********************/

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"兄得  没有这个key: %@",key);
    return nil;
}

/******************** 异常回调    ********************/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"兄嘚 没有这个  %@ 的set方法  value: %@",key, value);
}

@end

//在VC中这样调用测试
- (void)mutableArrayPatternStudy {
    LCPerson *person    = LCPerson.new;
    NSMutableArray *flruits = [person mutableArrayValueForKey:@"flruits"];
    [flruits addObject:@"banana"];
    NSLog(@"%@",flruits);
}

Screen Shot 2022-06-20 at 11.32.05.png

我们发现打印结果都是去发送我们成对实现中的插入方法,由此可以得以验证第一步中的描述。 如果没有成对实现或者干脆就没实现呢?那不就是直接走第二步了么?

@interface LCPerson : NSObject 
@property (nonatomic, strong)   NSArray *flruits;
@end


@implementation LCPerson
- (instancetype)init
{
    self = [super init];
    if (self) {
        _flruits = @[@"Apple"];
    }
    return self;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

/********************  异常回调    ********************/

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"兄得  没有这个key: %@",key);
    return nil;
}

/******************** 异常回调    ********************/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"兄嘚 没有这个  %@ 的set方法  value: %@",key, value);
}

@end

//在VC中这样调用测试
- (void)mutableArrayPatternStudy {
    LCPerson *person    = LCPerson.new;
    NSMutableArray *flruits = [person mutableArrayValueForKey:@"flruits"];
    [flruits addObject:@"banana"];
    NSLog(@"%@",flruits);
}

Screen Shot 2022-06-20 at 11.37.29.png

额...,这好像没有任何问题都没有走异常回调,不仅有苹果还把香蕉都添加进去。那是什么原理呢?

原理就在于,我们定义的属性,系统帮我们实现了set和get方法,而且还有带下滑线的实例变量,这就是我们为什么能在.m中没有定义下滑线实例变量的情况下就能使用下滑线实例变量的原因。第二、三步其实就是普通的setValueForKey过程,在这里不在赘述。

那如何验证走到异常的位置呢?对于我们命名的属性系统会自动添加set、get方法,如果把这一自动添加的方式给禁掉不就能复现这种场景了么?我们把上面的代码稍微改一下,看看运行结果

@interface LCPerson : NSObject 
@property (nonatomic, strong)   NSArray *flruits;
@end


@implementation LCPerson
@dynamic flruits;
- (instancetype)init
{
    self = [super init];
    if (self) {
    }
    return self;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

/********************  异常回调    ********************/

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"兄得  没有这个key: %@",key);
    return nil;
}

/******************** 异常回调    ********************/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"兄嘚 没有这个  %@ 的set方法  value: %@",key, value);
}

@end

//在VC中这样调用测试
- (void)mutableArrayPatternStudy {
    LCPerson *person    = LCPerson.new;
    NSMutableArray *flruits = [person mutableArrayValueForKey:@"flruits"];
    [flruits addObject:@"banana"];
    NSLog(@"%@",flruits);
}

Screen Shot 2022-06-20 at 14.01.46.png 我们看到,直接走的异常回调打印了。只是我重写了异常回调方法,应用并没有立即闪退但是打印了一些很有意思的log。各位读者有兴趣也可以自己发散研究研究。

  • 我们看到香蕉竟然被添加进去且打印出来。说明确实是返回了一个可变数组代理对象。
  • 数组的类型很奇特 [NSKeyValueSlowMutableArray:0x600002de2970 description],这类型看上去就跟提到的温馨提示很吻合。

3.6. 可变有序集合的取值过程

这个可变集合的设值过程和一般的设置过程查询方式有类似之处。请下下面的步骤

第一步,先查询有没有下面的成对方法出现,也就是说如果满足同时具备一个插入和删除方法就会返回一个代理对象来转发对可变有序集合的消息给接收 mutableOrderedSetValueForKey:消息的原始对象。同时如果原始对象中如果有实现replaceObjectIn<Key>AtIndex:withObject: or replace<Key>AtIndexes:with<Key>:方法,代理对象也会使用。

insertObject:in<Key>AtIndex:  removeObjectFrom<Key>AtIndex:
insert<Key>:atIndexes:        remove<Key>AtIndexes:

以上等同于下面 NSMutableOrderedSet 的方法
insertObjects:atIndexes:      removeObjectsAtIndexes:

第二步,如果没有找到可变集合的上述set方法,接下来开始查找像 set<Key>:这类方法访问器。返回的代理对象会是对接收 mutableOrderedSetValueForKey:消息的原始对象发送 set<Key>:消息。 对于这一步苹果官方有个温馨提示跟可变数组是一样的

官方温馨提示:虽然会有这一步可以走,但是此步骤中描述的机制比上一步的机制效率低得多,
因为它可能涉及反复创建新的集合对象,而不是修改现有的集合对象。
因此,在设计自己的键值编码兼容对象时,您通常应该避免使用它

第三步,如果既没有可变集合的相关方法也没有访问器方法,如果原始接收者的类方法accessInstanceVariablesDirectly 返回的是 YES 那么将会按照 _<key> or <key> 顺序查找实例变量。如果找到相应的实例变量,那么返回的代理对象将会把后续所有的可变集合的消息都转发给该实例变量,通常这个实例变量是可变集合或者是可变结合的子类。

第四步,如果以上都失败了,返回的代理对象会发送 setValue:forUndefinedKey: 消息给接收 mutableOrderedSetValueForKey:消息的原始对象。此消息会触发异常闪退,在实际开发中要重写此方法来截获异常闪退。

3.6.1. 可变有序集合取值过程demo取证
@interface LCPerson : NSObject 
@property (nonatomic, strong)   NSOrderedSet *orderedSet;
@end

@implementation LCPerson
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (void)insertObject:(id)object inOrderedSetAtIndex:(NSUInteger)index {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

- (void)removeOrderedSetObject:(id)object {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

- (void)insertOrderedSet:(NSArray *)array atIndexes:(NSIndexSet *)indexes{
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

- (void)removeOrderedSetAtIndexes:(NSIndexSet *)indexes {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

/********************  取值步骤第六步 异常回调    ********************/
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"兄得  没有这个key: %@",key);
    return nil;
}

/********************  设值步骤第三步 异常回调    ********************/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"兄嘚 没有这个  %@ 的set方法  value: %@",key, value);
}
@end 

在VC 中如下调用
- (void)mutableOrderedSetPatternStudy {
    LCPerson *person    = LCPerson.new;
    NSMutableOrderedSet *flruits = [person mutableOrderedSetValueForKey:@"orderedSet"];
    [flruits addObject:@"banana"];
    NSLog(@"%@",flruits);
}

代码我就不做多种组合粘贴了,此种情况跟可变数组非常类似,我将不同的组合的打印结果截图进行对比

  • insertObject:inOrderedSetAtIndex:index 实现+ 一个移除方法 Screen Shot 2022-06-20 at 15.05.20.png
  • insertOrderedSet:atIndexes: 实现 + 一个移除方法

Screen Shot 2022-06-20 at 15.08.25.png

  • 没有成对的插入和移除方法的时候 Screen Shot 2022-06-20 at 15.08.54.png
  • 禁掉自动为属性添加set、get方法 以及_key 实例变量的时候 加上@dynamic orderedSet;这句代码,同时要注释掉VC中使用set方法的代码,不然会报错。

Screen Shot 2022-06-20 at 15.20.30.png

3.7. 可变集合的取值过程

mutableSetValueForKey:的默认实现,给定一个key参数作为输入,使用以下过程返回接收访问器调用的对象内名为key的数组属性的可变代理集:

第一步,查找类似名名为下面成对的方法,如果找打了至少有一个添加方法和一个移除方法,那么,返回的代理对象会将 NSMutableSet所有的消息都转发给接收mutableSetValueForKey:的原始对象

add<Key>Object:               remove<Key>Object:
add<Key>:                     remove<Key>:

以上等同于下面 NSMutableSet 的方法
addObject: and `removeObject:
unionSet: and `minusSet:

第二步,如果接收mutableSetValueForKey:消息的对象为一个托管对象,就不继续往下走了。为什么呢?因为对于托管对象已经自动为它实现了键值编码属性值有效性验证拓展。参考指南Core Data Programming Guide

第三步,如果没有搜索到NSMutableSet的相关方法,同时接收原始mutableSetValueForKey:消息的对象也不是托管对象,就会去搜索set<Key>:方法。如果找到了这个方法,接下来返回的可变集合代理接收到的所有消息都会转发给接收原始mutableSetValueForKey:消息的对象。

官方温馨提示:虽然会有这一步可以走,但是此步骤中描述的机制比上一步的机制效率低得多,
因为它可能涉及反复创建新的集合对象,而不是修改现有的集合对象。
因此,在设计自己的键值编码兼容对象时,您通常应该避免使用它

第四步, 如果既没有可变集合的相关方法也没有访问器方法,如果原始接收者的类方法accessInstanceVariablesDirectly 返回的是 YES 那么将会按照 _<key> or <key> 顺序查找实例变量。如果找到相应的实例变量,那么返回的代理对象将会把后续所有的可变集合的消息都转发给该实例变量,通常这个实例变量是可变集合或者是可变结合的子类。

第五步,如果以上都不行,那么返回的代理对象会通过给接收mutableSetValueForKey:的原始对象发送setValue:forUndefinedKey: 消息来响应所有的 NSMutableSet的消息。同样如果没有重新该异常回调方法都会触发异常闪退

3.7.1. 可变集合的取值过程验证demo
@interface LCAAPersonEntityMO : NSManagedObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@property (nonatomic, strong) NSSet *set;
@end

@implementation LCAAPersonEntityMO
@dynamic name,age,set;

- (void)insertObject:(id)object inSetAtIndex:(NSUInteger)index {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

- (void)removeSetObject:(id)object {
    NSLog(@"%@ %s",NSStringFromClass([self class]) ,__func__);
}

/********************  取值步骤第六步 异常回调    ********************/
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"兄得  没有这个key: %@",key);
    return nil;
}

/********************  设值步骤第三步 异常回调    ********************/
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"兄嘚 没有这个  %@ 的set方法  value: %@",key, value);
}

@endVC中如此调用
- (void)mutableSetPatternStudy {
    LCAAPersonEntityMO *employee = [NSEntityDescription  insertNewObjectForEntityForName:@"LCPersonEntity" inManagedObjectContext:[self managedObjectContext]];
    employee.name = @"JasonPang";
    employee.age = 28;

 
    NSMutableSet *mutableSet = [employee mutableSetValueForKey:@"set"];
    [mutableSet addObject:@"hh"];

    NSLog(@"%@",mutableSet);
}

//此处省略coredata的准备过程,有兴趣深入研究的读者可参考指南进行自行添加

在这个取证过程中我们重点来验证一下第2步,其他的跟orderedSet是一致的读者有兴趣研究可以参考一下NSMutalbleOrderedSet相关代码。我们创建了一个托管对象 LCAAPersonEntityMO,添加了一个 set属性,调用测试代码打印结果如下: Screen Shot 2022-06-21 at 17.13.18.png 通过以上结果,我们发现它甚至都没走插入方法查找的第一步,直接闪退也没有走重写的异常。

3.8. 可变集合、数组搜索模型的反思总结

  • 1、你可以对一个不可变数组、集合进行可变操作,前提示这个消息接收对象必须有这个Key值的属性。
  • 2、确保接收消息的原始对象不是托管对象,托管对象不按正常套路走,自己实现了一套属性有效性验证逻辑,连异常都是走自己的。
  • 3、如果你想对一个不可变数组、集合通过KVC进行可变操作,需要确保至少实现了一对插入和删除方法,差一点也要确保实现set、get方法,尽量避免走到setValue:forKey的流程,因为效率太差。
  • 4、以防万一,使用对于使用KVC方式的对象最好确保重写了异常回调方法,避免异常闪退。

4. 自定义KVC

既然研究到这个份上了,我们不妨自己动手实现KVC。我简单写了大部分适用场景,读者有兴趣可以把代码拿去自己研究拓展。

@interface NSObject (LCKVC)
- (void)lc_setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)lc_valueForKey:(NSString *)key;
@end

#import "NSObject+LCKVC.h"
#import <objc/runtime.h>
@implementation NSObject (LCKVC)
- (void)lc_setValue:(nullable id)value forKey:(NSString *)key {
    // 1. key 非空判断
    if (!key || key.length == 0) {
        return;
    }

    // 2. 找到相关set方法 set<Key> _set<Key> setIs<Key>
    NSString * Key = key.capitalizedString; //首字母大写
  
    //拼接set方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    //判断是否有对应的set方法,如果有,直接调用
    if ([self lc_performSelectorWithMethonName:setKey value:value]) {
        NSLog(@"*************  %@ *******************",setKey);
        return;

    } else if ([self lc_performSelectorWithMethonName:_setKey value:value]) {
        NSLog(@"*************  %@ *******************",_setKey);
        return;
    } else if ([self lc_performSelectorWithMethonName:setIsKey value:value]) {
        NSLog(@"*************  %@ *******************",setIsKey);
        return;
    }

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

    // 4. 查找相关的实例变量进行赋值
    // 4.1 收集所有的实例变量
    NSMutableArray *ivarArray = [self getIvarListName];
    NSString *_key      = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey    = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey     = [NSString stringWithFormat:@"is%@",Key];

    // 遍历以上实例变量类型,如果有匹配上的就赋值,结束流程
    if ([ivarArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    } else if ([ivarArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    } else if ([ivarArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        object_setIvar(self, ivar, value);
        return;
    }

    // 5:如果找不到相关实例

    @throw [NSException exceptionWithName:@"LCUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
- (nullable id)lc_valueForKey:(NSString *)key {
    // 1.非空判断
    if (!key || key.length == 0) {
        return nil;
    }
    // 2.找到相关的 get方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex
    NSString *Key = key.capitalizedString;
    // 拼接方法
    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 = [[self performSelector:NSSelectorFromString(countOfKey)] intValue];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = [[self performSelector:NSSelectorFromString(countOfKey)] intValue];
            }
            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.判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"LCUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }

    // 4.找相应的实例变量进行赋值
    // 4.1 收集实例变量
    NSMutableArray *ivarArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([ivarArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);
    } else if ([ivarArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);
    } else if ([ivarArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);
    } else if ([ivarArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);
    }

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

- (BOOL)lc_performSelectorWithMethonName:(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;
}

@end

5. 你可能从来都没关注的KVC奇淫技巧

一旦熟练掌握这些技巧,你会发现。WC! 原来我花那么大力气写那么多代码人家一句现成的代码就解决了。关键是你居然以前都不知道

/// 数组操作
@interface LCHero : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *hArray;
@property (nonatomic, strong) NSMutableArray *mHArray;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int blood;
@property (nonatomic, assign) int power;
@property (nonatomic, strong) NSSet *set;
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSMutableArray *namesArrM;
@property (nonatomic, strong) NSMutableSet *namesSetM;
@property (nonatomic, strong) NSMutableOrderedSet *orderedSetM;


@end

- (void)arrayDemo {
    LCHero *h = [LCHero new];
    h.hArray = @[@"hero0",@"hero1",@"hero2",@"hero3",@"hero4"];
    NSArray *arr = [h valueForKey:@"heros"];
    NSLog(@"heros = %@", arr);
    NSLog(@"%d", [arr containsObject:@"hero3"]);
    NSEnumerator *enumerator = [arr objectEnumerator];
    NSString *str = nil;
    while (str == [enumerator nextObject]) {
        NSLog(@"%@", str);
    }
}

/// 字典操作
- (void)dictionaryTest {
    LCHero *h = LCHero.new;
    NSDictionary *dict = @{@"name":@"Jason",@"nick":@"胖大厨",@"subject":@"iOS",@"power":@10000,@"blood":@30000};
    [h setValuesForKeysWithDictionary:dict];
    NSLog(@"%@",h);
    NSArray *array = @[@"name",@"nick"];
    NSDictionary *dic = [h dictionaryWithValuesForKeys:array];
    NSLog(@"%@",dic);
}

/// KVC消息传递
- (void)arrayMessagePass {
    NSArray *array = @[@"Jason",@"Koddy",@"Kitty",@"John"];
    NSArray *lenStr = [array valueForKeyPath:@"length"];
    NSLog(@"%@",lenStr);
    NSArray *lowStr = [array valueForKeyPath:@"lowercaseString"];
    NSLog(@"%@",lowStr);
}

/// 聚合操作符@avg、@count、@max、@min、@sum
- (void)aggregationOperator {
    NSMutableArray *personArray = @[].mutableCopy;
    for (int i = 0; i < 6; i++) {
        LCHero *h = LCHero.new;
        NSDictionary *dict = @{@"name":@"Jason",@"nick":@"胖大厨",@"subject":@"iOS",@"power":@(10000 + i),@"blood":@(30000 + arc4random_uniform(300))};
        [h setValuesForKeysWithDictionary:dict];
        [personArray addObject:h];
    }
    NSLog(@"%@", [personArray valueForKey:@"blood"]);
    float avg = [[personArray valueForKeyPath:@"@avg.blood"] floatValue];
    NSLog(@"%f",avg);
    int count = [[personArray valueForKeyPath:@"@count.blood"] floatValue];
    NSLog(@"%d",count);

    int sum = [[personArray valueForKeyPath:@"@sum.blood"] floatValue];
    NSLog(@"%d",sum);
    
    int max = [[personArray valueForKeyPath:@"@max.blood"] floatValue];
    NSLog(@"%d",max);
    int min = [[personArray valueForKeyPath:@"@min.blood"] floatValue];
    NSLog(@"%d",min);
}

/// 数组操作符 @distinctUnionOfObjects @unionOfObjects
- (void)arrayOperator {
    NSMutableArray *personArray = @[].mutableCopy;
    for (int i = 0; i < 6; i++) {
        LCHero *h = LCHero.new;
        NSDictionary *dict = @{@"name":@"Jason",@"nick":@"胖大厨",@"subject":@"iOS",@"power":@(10000 + i),@"blood":@(30000 + arc4random_uniform(300))};
        [h setValuesForKeysWithDictionary:dict];
        [personArray addObject:h];
    }
    NSLog(@"%@", [personArray valueForKey:@"blood"]);
    //返回操作对象指定属性的集合 unionOfObjects
    NSArray *arr1 = [personArray valueForKeyPath:@"@unionOfObjects.blood"];
    NSLog(@"%@",arr1);
    // 返回操作对象指定属性的集合 -- 去重 //distinctUnionOfObjects.
    NSArray *arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.blood"];
    NSLog(@"%@",arr2);
}

/// 嵌套集合(array&set)操作 @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets
- (void)arrayNesting {
    NSMutableArray *personArray1 = @[].mutableCopy;
    for (int i = 0; i < 6; i++) {
        LCHero *h = LCHero.new;
        NSDictionary *dict = @{@"name":@"Jason",@"nick":@"胖大厨",@"subject":@"iOS",@"power":@(10000 + i),@"blood":@(30000 + arc4random_uniform(12))};
        [h setValuesForKeysWithDictionary:dict];
        [personArray1 addObject:h];
    }
    
    NSMutableArray *personArray2 = @[].mutableCopy;
    for (int i = 0; i < 6; i++) {
        LCHero *h = LCHero.new;
        NSDictionary *dict = @{@"name":@"Jason",@"nick":@"胖大厨",@"subject":@"iOS",@"power":@(10000 + i),@"blood":@(30000 + arc4random_uniform(12))};
        [h setValuesForKeysWithDictionary:dict];
        [personArray2 addObject:h];
    }

    //去重
    NSArray *nestArr = @[personArray1, personArray2];
    NSArray *arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.blood"];
    NSLog(@"arr = %@", arr);

    //简单相加
    NSArray *arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.blood"];
    NSLog(@"arr1 = %@", arr1);
}

- (void)setNesting {
    NSMutableSet *personSet1 = [NSMutableSet set];
    for (int i = 0; i < 6; i++) {
        LCHero *h = LCHero.new;
        NSDictionary *dict = @{@"name":@"Jason",@"nick":@"胖大厨",@"subject":@"iOS",@"power":@(10000 + i),@"blood":@(30000 + arc4random_uniform(12))};
        [h setValuesForKeysWithDictionary:dict];
        [personSet1 addObject:h];
    }
    NSLog(@"personSet1 = %@",[personSet1 valueForKey:@"blood"]);
    NSMutableSet *personSet2 = [NSMutableSet set];
    for (int i = 0; i < 6; i++) {
        LCHero *h = LCHero.new;
        NSDictionary *dict = @{@"name":@"Jason",@"nick":@"胖大厨",@"subject":@"iOS",@"power":@(10000 + i),@"blood":@(30000 + arc4random_uniform(12))};
        [h setValuesForKeysWithDictionary:dict];
        [personSet2 addObject:h];
    }
    NSLog(@"personSet2 = %@",[personSet2 valueForKey:@"blood"]);

    NSSet *nestSet = [NSSet setWithObjects:personSet1,personSet2, nil];
    NSLog(@"nestSet = %@",nestSet);
  
    NSArray *arr1 = [nestSet valueForKeyPath:@"@distinctUnionOfSets.blood"];
    NSLog(@"arr1 = %@",arr1);
}