iOS 底层探索:KVC 底层原理
前言
- KVC用的好多,今天来看看他的底层是怎么实现的。这里底层流程看起来有些粗糙,了解就行;
- KVC机制是在Foundation框架中,而Foundation框架是不开源的,可以看
官方文档: Key-Value Coding Programming Guide
Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.
内容
- KVC定义及相关API
- KVC 设值 底层原理
- KVC 取值 底层原理
- KVC 使用场景
- 拓展: 异常处理、 属性验证、 KVC处理非对象
一、 KVC定义及相关API
KVC
的全称是Key-Value Coding
,翻译成中文是 键值编码
,键值编码是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用该协议来间接访问其属性。既可以通过一个字符串key来访问某个属性。这种间接访问机制补充了实例变量及其相关的访问器方法所提供的直接访问。
主要有以下四个常用的方法:
//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;
//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
其他API:
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_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<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
二、KVC 设值 底层原理
针对赋值,Key-Value Coding Programming Guide中有这样一段
Search Pattern for the Basic Setter
The default implementation of setValue:forKey:, given key and value parameters as input, attempts to set a property named key to value (or, for non-object properties, the unwrapped version of value, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:
Look for the first accessor named set<Key>: or _set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.
If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.
Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
对于setValue:forKey:给定的key和value参数作为输入,尝试设置命名属性key到value主要流程为
- 按顺序查找名为set<Key>或 _set<Key>方法,如果找到,直接调用。如果没找到,则第2步
- 调用类方法accessInstanceVariablesDirectly 如果返回Yes,则进入间接访问实例化赋值,寻找名为_<key>,_is<Key>,<key>,或者is<Key>的变量,如果找到,则直接使用输入值设置变量。
- 如果返回NO或者没找到上面的值,则调用setValue:forUndefinedKey:,默认会抛出异常。
举个例子:
LGPerson *person = [[LGPerson alloc] init];
// 1、一般setter 方法
person.name = @"AA";
// 2、KVC方式
[person setValue:@"CC" forKey:@"name"];
流程大概如下图:
kvc 赋值流程图.png
二、KVC 取值 底层原理
针对赋值,Key-Value Coding Programming Guide中有这样一段
Accessor Search Patterns
The default implementation of the NSKeyValueCoding protocol provided by NSObject maps key-based accessor calls to an object’s underlying properties using a clearly defined set of rules. These protocol methods use a key parameter to search their own object instance for accessors, instance variables, and related methods that follow certain naming conventions. Although you rarely modify this default search, it can be helpful to understand how it works, both for tracing the behavior of key-value coded objects, and for making your own objects compliant.
NOTE
The descriptions in this section use <key> or <Key> as a placeholder for the key string that appears as a parameter in one of the key-value coding protocol methods, which is then used by that method as part of a secondary method call or variable name lookup. The mapped property name obeys the placeholder’s case. For example, for the getters <key> and is<Key>, the property named hidden maps to hidden and isHidden.
Search Pattern for the Basic Getter
The default implementation of valueForKey:, given a key parameter as input, carries out the following procedure, operating from within the class instance receiving the valueForKey: call.
Search the instance for the first accessor method found with a name like get<Key>, <key>, is<Key>, or _<key>, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.
If no simple accessor method is found, search the instance for methods whose names match the patterns countOf<Key> and objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) and <key>AtIndexes: (corresponding to the NSArray method objectsAtIndexes:).
If the first of these and at least one of the other two is found, create a collection proxy object that responds to all NSArray methods and return that. Otherwise, proceed to step 3.
The proxy object subsequently converts any NSArray messages it receives to some combination of countOf<Key>, objectIn<Key>AtIndex:, and <key>AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get<Key>:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.
If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class).
If all three methods are found, create a collection proxy object that responds to all NSSet methods and return that. Otherwise, proceed to step 4.
This proxy object subsequently converts any NSSet message it receives into some combination of countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.
If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.
If the retrieved property value is an object pointer, simply return the result.
If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.
If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.
If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
对于取值,文档中有以上说明,其实类似于上面的赋值原理。
在valueForKey:给定key参数作为输入的情况下
- 首先查找getter方法,按照get<Key> -> <key> -> is<Key> -> _<key>的方法顺序查找 如果找到则进入步骤5,如果没找到,则下一步
- 如果步骤1中的getter方法没有找到,KVC会查找countOf <Key>和objectIn <Key> AtIndex :和<key> AtIndexes : 如果找到其中的第一个以及其他两个中的至少一个,则创建一个响应所有NSArray方法的集合代理对象,并返回该对象。否则,请继续执行步骤3。代理对象随后将任何NSArray接收到的一些组合的消息countOf<Key>,objectIn<Key>AtIndex:和<key>AtIndexes:消息给KVC创建它兼容的对象。如果原始对象还实现了名称为的可选方法get<Key>:range:,则代理对象也会在适当时使用该方法。
- 如果没有找到简单的访问方法或阵列访问方法组,寻找一个三重的方法命名countOf<Key>,enumeratorOf<Key>和memberOf<Key>:(对应于由所定义的原始的方法NSSet类)。如果找到所有三个方法,请创建一个响应所有NSSet方法的集合代理对象并将其返回。否则,请继续执行步骤4。
- 如果此时还未找到,且接收器的类方法accessInstanceVariablesDirectly返回YES,搜索名为实例变量_<key>,_is<Key>,<key>,或者is<Key>,按照这个顺序。如果找到,请直接获取实例变量的值,然后继续执行步骤5。否则,请继续执行步骤6。
- 如果检索到的属性值是对象指针,则只需返回结果。
如果该值是所支持的标量类型NSNumber,则将其存储在NSNumber实例中并返回该实例。
如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。- 如果其他所有方法均失败,请调用valueForUndefinedKey:。默认情况下会引发异常
流程大概如下图:
三、KVC 使用场景
估计都会使用就不举例了
-
利用KVC动态的取值和设值
常用的可以通过setValue:forKey: 和 valueForKey:
也可以通过路由的方式setValue:forKeyPath: 和 valueForKeyPath:
-
Model和字典转换
-
用KVC来访问和修改私有变量
根据上面的实现原理我们知道,
KVC本质上是操作方法列表以及在内存中查找实例变量
。我们可以利用这个特性访问类的私有变量,例如下面在.m中定义的私有成员变量和属性,都可以通过KVC的方式访问。【对于KVC
而言,一个对象没有自己的隐私
,所以可以通过KVC修改和访问任何私有属性
】这个操作对readonly的属性,@protected的成员变量,都可以正常访问。如果不想让外界访问类的成员变量,则可以将
accessInstanceVariablesDirectly
属性赋值为NO
。 -
修改一些控件的内部属性
这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些控件的API(常用的就是自定义
tabbar
、个性化UITextField
中的placeHolderText
),这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。 -
用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);
}
}
//********打印结果********
2020-10-27 11:33:43.377672+0800 CJLCustom[60035:6380757] English
2020-10-27 11:33:43.377773+0800 CJLCustom[60035:6380757] Franch
2020-10-27 11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese
2020-10-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7
2020-10-27 11:33:43.378327+0800 CJLCustom[60035:6380757] 6
2020-10-27 11:33:43.378417+0800 CJLCustom[60035:6380757] 7
四、拓展
KVC属性验证
KVC提供了属性值,用来验证key对应的Value是否可用的方法
- 在调用KVC时可以先进行验证,验证通过下面两个方法进行,支持
key
和keyPath
两种方式。验证方法默认实现返回YES,可以通过重写对应的方法修改验证逻辑。
验证方法需要我们手动调用,并不会在进行KVC
的过程中自动调用。
- (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;
这个方法的默认实现是去探索类里面是否有一个这样的方法:
-(BOOL)validate<Key>:error:
如果有这个方法,就调用这个方法来返回,没有的话就直接返回YES
@implementation Address
-(BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{ //在implementation里面加这个方法,它会验证是否设了非法的value
NSString* country = *value;
country = country.capitalizedString;
if ([country isEqualToString:@"Japan"]) {
return NO; //如果国家是日本,就返回NO,这里省略了错误提示,
}
return YES;
}
@end
NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //如果没有重写-(BOOL)-validate<Key>:error:,默认返回Yes
if (result) {
NSLog(@"键值匹配");
[add setValue:value forKey:key];
}
else{
NSLog(@"键值不匹配"); //不能设为日本,其他国家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);
//打印结果
KVCDemo[867:58871] 键值不匹配
KVCDemo[867:58871] country:China
KVC处理非对象
KVC是支持基础数据类型和结构体的,可以在setter和getter的时候,通过NSValue和NSNumber来转换为OC对象。该方法valueForKey:总是返回一个id对象,如果原本的变量类型是值类型或者结构体,返回值会封装成NSNumber或者NSValue对象。这两个类会处理从数字,布尔值到指针和结构体任何类型。然后开发者需要手动转换成原来的类型。尽管valueForKey:会自动将值类型封装成对象,但是setValue:forKey:却不行。你必须手动将值类型转换成NSNumber或者NSValue类型,才能传递过去。
- 可以调用initWithBool:方法对基础数据类型进行包装
@property (nonatomic, assign, readonly) BOOL boolValue;
- (NSNumber *)initWithBool:(BOOL)value
KVC异常处理
- key或者keyPath发生错误
当根据KVC搜索规则,没有搜索到对应的key或者keyPath,则会调用对应的异常方法。异常方法的默认实现,在异常发生时会抛出一个NSUndefinedKeyException的异常,并且应用程序Crash
我们可以重写下面两个方法:
//获取了不存在的key 只需要实现如下方法
- (nullable id)valueForUndefinedKey:(NSString *)key;
//设置了不存在的key 造成崩溃 只需要实现如下方法
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- 传参为nil
通常情况下,KVC不允许你要在调用setValue:属性值 forKey:(或者keyPath)时对非对象传递一个nil的值。因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
- (void)setNilValueForKey:(NSString *)key{
NSLog(@"-----设置了nil");
}