iOS之深入解析KVC的底层原理和自定义KVC的实现

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析KVC的底层原理和自定义KVC的实现相关的知识,希望对你有一定的参考价值。

一、KVC 简介

① 定义
  • KVC 是 Key-Value Coding 的简称,中文译义为键值编码。
  • KVC 是指 ios 的开发中,可以允许开发者通过 Key 名直接访问对象的属性,或者给对象的属性赋值,而不需要调用明确的存取方法。
  • KVC 是由 NSKeyValueCoding 非正式协议启用的一种机制,对象采用该协议来间接访问其属性,即可以通过一个字符串 key 来访问某个属性。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。
  • 通过 KVC 就可以在运行时动态地访问和修改对象的属性,而不是在编译时确定,这也是 iOS 开发中的黑魔法之一,很多高级的 iOS 开发技巧都是基于 KVC 实现的。
② 相关方法
  • 在 NSKeyValueCoding 中提供了 KVC 通用的访问方法,分别是 getter 方法 valueForKey: 和 setter 方法 setValue:forKey:,以及其衍生的 keyPath 方法,这两个方法对各个类均通用。并且由 KVC 提供默认的实现,我们也可以自己重写对应的方法来改变实现。
  • 在 NSKeyValueCoding 中,KVC 最为重要的方法如下:
	// 通过key来取值
	- (id)valueForKey:(NSString *)key;
	
	// 通过keyPath来取值
	- (id)valueForKeyPath:(NSString *)keyPath;
	
	// 通过key来设值
	- (void)setValue:(id)value forKey:(NSString *)key;
	
	// 通过keyPath来设值
	- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
  • NSKeyValueCoding 中还有其它的相关方法,例如:
	// KVC提供属性值确认的API,它可以用来检查set的值是否正确,为不正确的值做一个替换值或者拒绝设值新值并返回错误原因
	- (BOOL)validateValue:(inout id  _Nullable *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable *)outError;
	
	// 如果key不存在,且没有KVC无法搜索到任何和key有关的字段或者属性,则会调用这个方法,默认是抛出异常
	- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
	
	// 和上一个方法一样,上一个方法为设值,该方法为取值
	- (id)valueForUndefinedKey:(NSString *)key;
	
	// 如果在setValue方法时给value传nil,则会调用该方法
	- (void)setNilValueForKey:(NSString *)key;
	
	// 输入一组key,返回该组key对应的value,再转成字典返回,用于将model转字典
	- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

二、KVC 使用

① 基础使用
  • KVC 主要对三种类型进行操作,基础数据类型及常量、对象类型、集合类型。
  • 在使用 KVC 时,直接将属性名当做 key,并设置 value,即可对属性进行赋值。如下:
	@interface YDWCar : NSObject
	
	@property (nonatomic, copy)   NSString *carNumber;
	@property (nonatomic, strong) NSNumber *mileage;
	@property (nonatomic, strong) YDWPerson *owner;
	@property (nonatomic, strong) NSArray<Performance *> *performances;
	
	@end
	
	YDWCar *car = [[YDWCar alloc] init];
	[car setValue:@(1000.00) forKey:@"mileage"];
  • 除了对当前对象的属性进行赋值外,还可以对其更“深层”的对象进行赋值。例如对当前对象的 owner 属性的 name 属性进行赋值,KVC 进行多级访问时,直接类似于属性调用一样用点语法进行访问即可。如下所示:
	[car setValue:@"YDW" forKeyPath:@"owner.name"];
  • 通过 keyPath 对数组进行取值时,并且数组中存储的对象类型都相同,可以通过 valueForKeyPath: 方法指定取出数组中所有对象的某个字段。例如,通过 valueForKeyPath: 将数组中所有对象的 name 属性值取出,并放入一个数组中返回。如下所示:
	NSArray *names = [array valueForKeyPath:@"name"];
② 多值操作使用
  • KVC 还有更强大的功能,可以根据给定的一组 key,获取到一组 value,并且以字典的形式返回,获取到字典后可以通过 key 从字典中获取到 value(需要注意的是,虽然看到 dictionary 的字样,但这里并不是字典的方法)。如下:
	- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
  • 同样,也可以通过 KVC 进行批量赋值。在对象调用 setValuesForKeysWithDictionary: 方法时,可以传入一个包含 key、value 的字典进去,KVC 可以将所有数据按照属性名和字典的 key 进行匹配,并将 value 给 User 对象的属性赋值。
	- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
③ 实用技巧
  • 在项目中经常会遇到字典转模型的情况,如果在自定义的 init 方法里逐个赋值,这样每次数据发生改变还需要改赋值语句,然而通过 KVC 提供的赋值 API,可以对数据进行批量赋值。
  • 假设有以下 JSON 数据并定义 User 类,在外界通过 setValuesForKeysWithDictionary:方法对 User 进行赋值。
	JSON:
	{
	    "userName": "YDW",
	    "age": 28,
	    "id": 100
	}
	
	@interface User : NSObject
	
	@property (nonatomic, copy)   NSString *name;
	@property (nonatomic, assign) NSInteger age;
	@property (nonatomic, assign) NSInteger userId;
	
	@end
	
	@implementation User
	
	- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
	    if ([key isEqualToString:@"id"]) {
	        self.userId = [value integerValue];
	    }
	}
	
	@end
  • 赋值时会遇到一些问题,例如服务器会返回一个 id 字段,但是对于客户端来说 id 是系统保留字段(关键字),可以重写 setValue:forUndefinedKey: 方法并在内部处理 id 参数的赋值。
  • 转换时需要服务器数据和类定义匹配,字段数量和字段名都应该匹配。如果 User 比服务器数据多,则服务器没传的字段为空;如果服务端传递的数据 User 中没有定义,则会导致崩溃。
  • 在 KVC 进行属性赋值时,内部会对基础数据类型做处理,不需要手动做 NSNumber 的转换。需要注意的是,NSArray 和 NSDictionary 等集合对象,value 都不能是 nil,否则会导致 Crash。
④ 异常信息
  • 根据 KVC 搜索规则,当没有搜索到对应的 key 或者 keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个 NSUndefinedKeyException 异常,并且应用程序 Crash。
  • 我们可以重写下面两个方法,根据业务需求合理的处理 KVC 导致的异常:
	- (nullable id)valueForUndefinedKey:(NSString *)key;
	- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
  • 当通过 KVC 给某个非对象的属性赋值为 nil 时,此时 KVC 会调用属性所属对象的 setNilValueForKey: 方法,并抛出 NSInvalidArgumentException 的异常,并使应用程序 Crash。
  • 我们可以通过重写下面方法,在发生这种异常时进行处理。例如给 name 赋值为 nil 的时候,就可以重写 setNilValueForKey: 方法并表示 name 是空的。
	- (void)setNilValueForKey:(NSString *)key {
	    if ([key isEqualToString:@"name"]) {
	        [self setValue:@"" forKey:@"name"];
	    } else {
	        [super setNilValueForKey:key];
	    }
	}
⑤ 集合属性
  • 根据 KVO 的实现原理,是在运行时生成新的子类并重写其 setter 方法,在其内容发生改变时发送消息。但这只是对属性直接进行赋值会触发,如果属性是容器对象,对容器对象进行 add 或 remove 操作,则不会调用 KVO 的方法。
  • 可以通过 KVC 对应的 API 来配合使用,使容器对象内部发生改变时也能触发 KVO。
  • 在进行容器对象操作时,先调用以下方法通过 key 或者 keyPath 获取集合对象,然后再对容器对象进行 add 或 remove 等操作时,就会触发 KVO 的消息通知。如下:
	// key 方法
	- (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;
	
	// keyPath 方法
	- (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;
⑥ 集合运算符
  • KVC 提供的 valueForKeyPath: 方法非常强大,可以通过该方法对集合对象进行“深入”操作,在其 keyPath 中嵌套集合运算符。例如求一个数组中对象某个属性的 count (集合对象指 NSArray 和 NSSet,但不包括NSDictionary):

在这里插入图片描述

  • 这个集合运算表达式主要分为三部分,left 部分是要操作的集合对象,如果调用 KVC 的对象本来就是集合对象,则 left 可以为空,中间部分是表达式,表达式一般以@符号开头,后面是进行运算的属性。
  • 集合运算符主要分为三类:
    • 集合操作符:处理集合包含的对象,并根据操作符的不同返回不同的类型,返回值以NSNumber为主。
    • 数组操作符:根据操作符的条件,将符合条件的对象包含在数组中返回。
    • 嵌套操作符:处理集合对象中嵌套其他集合对象的情况,返回结果也是一个集合对象。
  • 继续“基础使用”中的例子,定义 Performance 类为模型类,类中包含三种类型的属性,并定义 YDWCar 类,其中包含一个数组,下面的代码示例就都是操作这个数组的,并且数组包含所有 Performance 对象。如下:
	@interface Performance : NSObject
	
	@property (nonatomic, strong) NSString *power;
	@property (nonatomic, strong) NSNumber *fuel;
	@property (nonatomic, strong) NSString *brake;
	
	@end

	@interface YDWCar : NSObject
	
	@property (nonatomic, retain) NSArray *performances;
	
	@end
  • 集合操作符处理 NSArray 和 NSSet 及其子类这样的集合对象,并根据不同的操作符返回不同类型的对象,返回值一般都是 NSNumber。
    • @avg 用来计算集合中 right keyPath 指定的属性的平均值:
	NSNumber *performanceAverage = [self.performances valueForKeyPath:@"@avg.fuel"];
    • @count 用来计算集合的总数(@count 操作符比较特殊,它不需要写right keyPath,即使写了也会被忽略):
	NSNumber *numberOfPerformances = [self.performances valueForKeyPath:@"@count"];
    • @sum 用来计算集合中 right keyPath 指定的属性的总和:
	NSNumber *amountSum = [self.performances valueForKeyPath:@"@sum.fuel"];
    • @max 用来查找集合中 right keyPath 指定的属性的最大值:
	NSString *maxBrake = [self.performances valueForKeyPath:@"@max.brake"];
    • @min 用来查找集合中 right keyPath 指定的属性的最小值:
	NSString *minBrake = [self.performances valueForKeyPath:@"@min.brake"];
    • @max 和 @min 在进行判断时,都是通过调用 compare: 方法进行判断,所以可以通过重写该方法对判断过程进行控制。
  • 数组操作符
    • @unionOfObjects 将集合对象中,所有 power 对象放在一个数组中并返回:
	NSArray *powers = [self.performances valueForKeyPath:@"@unionOfObjects.power"];
    • @distinctUnionOfObjects 将集合对象中,所有 power 对象放在一个数组中,并将数组进行去重后返回:
	NSArray *distinctPower = [self.performances valueForKeyPath:@"@distinctUnionOfObjects.power"];
    • 以上两个方法中,如果操作的属性为 nil,在添加到数组中时会导致 Crash。
  • 嵌套操作符
    • 由于嵌套操作符是需要对嵌套的集合对象进行操作,所以新建一个 arrayOfArrays 对象,其中包含两个数组,数组中存储的都是 Performance 类型对象。
	NSArray *morePerformance = ....;
	NSArray *arrayOfArrays = @[self.performances, morePerformance];
    • @unionOfArrays 是用来操作集合内部的集合对象,将所有 right keyPath 对应的对象放在一个数组中返回:
	NSArray *collectedPower = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.power"];
    • @distinctUnionOfArrays 是用来操作集合内部的集合对象,将所有 right keyPath 对应的对象放在一个数组中,并进行排重:
	NSArray *collectedDistinctPower = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.power"];
    • @distinctUnionOfSets 是用来操作集合内部的集合对象,将所有 right keyPath 对应的对象放在一个 set 中,并进行排重:
	NSSet *collectedPower = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfSets.power"];
⑦ 非对象值处理
  • KVC 是支持基础数据类型和结构体的,可以在 setter 和 getter 的时候,通过 NSValue 和 NSNumber 来转换为 OC 对象,Swift 中不存在这样的需求,因为 Swift 中所有变量都是对象。
  • 结构体的转换,可以调用 initWithBool: 方法对基础数据类型进行包装,除了调用方法外还可以通过字面量实现,例如 @(YES) 的调用,通过 NSNumber 的 boolValue 属性转换为基础数据类型:
	@property (nonatomic, assign, readonly) BOOL boolValue;
	- (NSNumber *)initWithBool:(BOOL)value NS_DESIGNATED_INITIALIZER;
  • 结构体转换的代码定义在 UIGeometry.h 中,以 NSValue 的 Category 形式存在,NSValue 对 CGPoint、CGRect 等结构体都提供了转换方法。例如对 CGPoint 进行转换:
	@property(nonatomic, assign, readonly) CGPoint CGPointValue;
	+ (NSValue *)valueWithCGPoint:(CGPoint)point;
  • 无论什么时候都不应该给 setter 中传入 nil,会导致 Crash 并引起 NSInvalidArgumentException 异常。
⑧ 属性验证
  • 在调用 KVC 时可以先进行验证,验证通过 validateValue 方法进行,支持 key 和 keyPath 两种方式,验证方法默认实现返回 YES,可以通过重写对应的方法修改验证逻辑。
	- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
	- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
  • 验证方法需要我们手动调用,并不会在进行 KVC 的过程中自动调用。
  • 在 validateValue 方法的内部实现中,如果传入的 value 或 key 有问题,可以通过返回 NO 来表示错误,并设置 NSError 对象。
	YDWPerson *person = [[YDWPerson alloc] init];
	NSError *error;
	NSString *name = @"dw";
	if (![person validateValue:&name forKey:@"name" error:&error]) {
	    NSLog(@"%@", error);
	}
  • KVC 还支持对单独属性做验证,可以通过定义 validate:error: 格式的方法,并在方法内部实现验证代码。在编写 KVC 验证代码的时候,应该先查找属性有没有自定义 validate 方法,然后再查找 validateValue: 方法,如果有则调用自己实现的方法,如果两个方法都没有实现则默认返回YES。
	- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
	    if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) {
	        if (outError != NULL) {
	            *outError = [NSError errorWithDomain:PersonErrorDomain
	                                            code:PersonInvalidNameCode
	                                        userInfo:@{ NSLocalizedDescriptionKey
	                                                    : @"Name too short" }];
	        }
	        return NO;
	    }
	    return YES;
	}
⑨ 搜索规则
  • KVC 在通过 key 或者 keyPath 进行操作的时候,可以查找属性方法、成员变量等,查找的时候可以兼容多种命名:在 KVC 的实现中,依赖 setter 和 getter 的方法实现,所以方法命名应该符合苹果要求的规范,否则会导致 KVC 失败。
  • 在学习 KVC 的搜索规则前,要先弄明白一个属性的作用,这个属性在搜索过程中起到很重要的作用,这个属性表示是否允许读取实例变量的值,如果为 YES 则在 KVC 查找的过程中,从内存中读取属性实例变量的值。
	@property (class, readonly) BOOL accessInstanceVariablesDirectly;
  • 基础 Getter 搜索模式是 valueForKey: 的默认实现:给定一个 key 当做输入参数,通过以下步骤,在这个接收 valueForKey: 方法调用的类内部进行操作:
    • 1.通过 getter 方法搜索实例,例如 get< Key >, < key >, is< Key >, _< key > 的拼接方案,按照这个顺序,如果发现符合的方法,就调用对应的方法并拿着结果跳转到第5步,否则,就继续到下一步。
    • 2.如果没有找到简单的 getter 方法,则搜索其匹配模式的方法countOf< Key >、objectIn< Key >AtIndex:、< key >AtIndexes:;如果找到其中的第一个和其他两个中的一个,则创建一个集合代理对象,该对象响应所有 NSArray 的方法并返回该对象,否则继续到第3步。代理对象随后将 NSArray 接收到的 countOf< Key >、objectIn< Key >AtIndex:、< key >AtIndexes:的消息给符合 KVC 规则的调用方。当代理对象和 KVC 调用方通过上面方法一起工作时,就会允许其行为类似于 NSArray 一样。
    • 3.如果没有找到 NSArray 简单存取方法,或者 NSArray 存取方法组,则查找有没有 countOf< Key >、enumeratorOf< Key >、memberOf< Key >: 命名的方法。如果找到三个方法,则创建一个集合代理对象,该对象响应所有 NSSet 方法并返回,否则继续执行第4步。此代理对象随后转换 countOf< Key >、enumeratorOf< Key >、memberOf< Key >: 方法调用到创建它的对象上,实际上,这个代理对象和 NSSet 一起工作,使得其表象上看起来是 NSSet。
    • 4.如果没有发现简单 getter 方法,或集合存取方法组,以及接收类方法 accessInstanceVariablesDirectly 是返回 YES 的,搜索一个名为 _< key >、_is< Key >、< key >、is< Key > 的实例,根据他们的顺序,如果发现对应的实例,则立刻获得实例可用的值并跳转到第5步,否则跳转到第6步。
    • 5.如果取回的是一个对象指针,则直接返回这个结果。如果取回的是一个基础数据类型,但是这个基础数据类型是被 NSNumber 支持的,则存储为 NSNumber 并返回。如果取回的是一个不支持 NSNumber 的基础数据类型,则通过 NSValue 进行存储并返回。
    • 6.如果所有情况都失败,则调用 valueForUndefinedKey: 方法并抛出异常,这是默认行为,但是子类可以重写此方法。
  • 基础 Setter 搜索模式是 setValue:forKey: 的默认实现,给定输入参数 value 和 key,在接收调用对象的内部,设置属性名为 key 的 value,通过下面的步骤:
    • 查找 set< Key >: 或 _set< Key > 命名的 setter,按照这个顺序,如果找到的话,调用这个方法并将值传进去(根据需要进行对象转换)。
    • 如果没有发现一个简单的 setter,但是 accessInstanceVariablesDirectly 类属性返回 YES,则查找一个命名规则为 _< key >、_is< Key >、< key >、is< Key > 的实例变量,根据这个顺序,如果发现则将 value 赋值给实例变量。
    • 如果没有发现 setter 或实例变量,则调用 setValue:forUndefinedKey: 方法,并默认提出一个异常,但是一个 NSObject 的子类可以提出合适的行为。
⑩ 安全检查
  • 在使用 KVC 时,由于传入的 key 或者 keyPath 是一个字符串,因此很容易写错或者属性本身修改后忘记修改对应的字符串,导致 crash。
  • 解决的方案为,利用反射机制,通过 @selector() 获取到方法的 SEL,然后通过 NSStringFromSelector() 将 SEL 反射为字符串,这样在 @selector() 中传入方法名的过程中,编译器会有合法性检查,如果方法不存在或者未实现时,会报对应的警告。
	[self valueForKey:NSStringFromSelector(@selector(object))];

三、KVC 底层分析

① KVC 设值
  • 设值会调用 setValue:forKey: 方法,其大致步骤如下流程图所示:

在这里插入图片描述

  • 流程分析:
    • 查找 set< Key >: 或 _set< Key >: 命名的 setter,按照这个顺序,如果找到,则调用这个方法并将值传进去。
    • 如果没有发现一个简单的 setter ,但是 accessInstanceVariablesDirectly 类属性返回 YES,则查找一个命名规则为 _key、_isKey、key、isKey 的实例变量。按照这个顺序,如果查找到则将 value 赋值给实例变量。
    • 如果没有找到 setter 或实例变量,则调用 setValue:forUndefinedKey: 方法,并默认抛出一个异常。
② KVC 取值
  • 当调用 valueForKey: 方法时,KVC 对 key 的搜索顺序有点不同于 setValue:forKey: 方法,大致步骤如下:

在这里插入图片描述

  • 流程分析:
    • 首先按 get< Key >、< key >、is< Key > 的顺序查找 getter 方法,找到直接调用。
      • 若方法的返回结果类型是一个对象指针,则直接返回结果。
      • 若类型为能够转化为 NSNumber 的基本数据类型,转换为 NSNumber 后返回;否则转换为 NSValue 返回。
    • 若上面的 getter 没有找到,则查找 countOf< Key >、objectIn< Key >AtIndex:、< Key >AtIndexes 格式的方法。如果 countOf< Key > 和另外两个方法中的一个找到,那么就会返回一个可以响应 NSArray 所有方法的集合代理。发送给这个代理集合的 NSArray 消息方法,就会以 countOf< Key >、objectIn< Key >AtIndex:、< Key >AtIndexes 这几个方法组合的形式调用。如果 receiver 的类实现了 get:range: 方法,该方法也会用于性能优化。
    • 还没查到,那么查找 countOf< Key >、enumeratorOf< Key >、memberOf< Key >: 格式的方法。如果这3个方法都找到,那么久返回一个可以相应 NSSet 所有方法的集合代理。发送给这个代理集合的 NSSet 消息方法,就会以countOf< Key >、enumeratorOf< Key >、memberOf< Key >: 组合的形式调用。
    • 还是没查到,那么如果类方法 accessInstanceVariablesDirectly 返回 YES,那么按_< key >、_is< Key >、< key >、is< Key > 的顺序直接搜索实例变量。如果搜索到了,则返回 receiver 相应实例变量的值。
    • 再没有查到,调用 valueForUndefinedKey: 方法,抛出异常。

四、自定义 KVC

① 原理
  • 通过给 NSObject 添加分类 YDWKVC,实现自定义的dw_setValue:forKey: 和 dw_valueForKey: 方法,根据苹果官方文档提供的查找规则进行实现。
  • YDWKVC 如下所示:
	@interface NSObject (YDWKVC)
	
	// 设值
	- (void)dw_setValue:(nullable id)value forKey:(NSString *)key;
	// 取值
	- (nullable id)dw_valueForKey:(NSString *)key;
	
	@end
② 自定义 KVC 设值
	- (void)dw_setValue:(nullable id)value forKey:(NSString *)key {
	    // 判断 key 是否存在
	    if (key == nil || key.length == 0) return;
	    
	    // 找 setter 方法,顺序是:setXXX、_setXXX、 setIsXXX 注意:key 要大写
	    NSString *Key = key.capitalizedString;
	    // key 要大写
	    NSString *setKey = [NSString stringWithFormat:@"set%@:", Key];
	    NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key];
	    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key];
	    
	    if ([self dw_performSelectorWithMethodName:setKey value:value]) {
	        NSLog(@"*************%@*************", setKey);
	        return;
	    } else if([self dw_performSelectorWithMethodName:_setKey value:value]){
	        NSLog(@"*************%@*************", _setKey);
	        return;
	    } else if([self dw_performSelectorWithMethodName:setIsKey value:value]){
	        NSLog(@"*************%@*************", setIsKey);
	        return;
	    }
	    
	    // 判断是否响应‘accessInstanceVariablesDirectly`方法,即间接访问实例变量,返回YES,继续下一步设值,如果是NO,则崩溃
	    if (![self.class accessInstanceVariablesDirectly]) {
	        @throw [NSException exceptionWithName:@"YDWUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
	    }
	    
	    // 间接访问变量赋值,顺序为:_key、_isKey、key、isKey
	    // 定义一个收集实例变量的可变数组
	    NSMutableArray *mArray = [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 ([mArray containsObject:_key]) {
	        // 获取相应的 ivar
	        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
	        // 对相应的 ivar 设置值以上是关于iOS之深入解析KVC的底层原理和自定义KVC的实现的主要内容,如果未能解决你的问题,请参考以下文章

iOS底层探索之KVC

iOS底层KVC原理

iOS底层KVC原理

iOS开发底层之KVC了解 - 16

iOS开发底层之KVC了解 - 16

iOS开发底层之KVC了解 - 16