KVC
属于Foundation
框架,不开源,我们只能通过官方文档来了解它
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(键值编码
)由 NSKeyValueCoding非正式协议
启用的一种机制,采用该协议可以间接访问
对象的属性。当一个对象与键值编码兼容时,它的属性可以通过一个简洁、统一的消息传递接口通过字符串参数寻址。这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。
Objects typically adopt key-value coding when they inherit from NSObject (directly or indirectly),
所有直接或者间接继承了NSObject的类型,也就是几乎所有的Objective-C对象都能使用KVC (一些纯Swift类和结构体是不支持KVC的)
KVC常用的四个方法
// 通过 key 设值 - (void)setValue:(nullable id)value forKey:(NSString *)key; // 通过 key 取值 - (nullable id)valueForKey:(NSString *)key; // 通过 keyPath 设值 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; // 通过 keyPath 取值 - (nullable id)valueForKeyPath:(NSString *)keyPath; 复制代码
NSKeyValueCoding类别中还有其他的一些方法
// 默认返回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; 复制代码
这段内容比较基础,只需要注意:只有继承于NSObject的数据才能使用KVC,非NSObject类型的需要做类型转换。
通过 valueForKey:
和 setValue:Forkey
:来间接的获取和设置属性值
valueForKey
: - Returns the value of a property named by the key parameter. If the property named by the key cannot be found according to the rules described in Accessor Search Patterns, then the object sends itself a valueForUndefinedKey: message. The default implementation of valueForUndefinedKey: raises an NSUndefinedKeyException, but subclasses may override this behavior and handle the situation more gracefully.
valueForKey
: 返回由 key 参数命名的属性的值。如果根据访问者搜索模式中的规则找不到由 key 命名的属性,则该对象将向自身发送 valueForUndefinedKey:
消息。valueForUndefinedKey:
的默认实现会抛出 NSUndefinedKeyException
异常,但是子类可以重写此行为并更优雅地处理这种情况。
setValue:forKey:
: Sets the value of the specified key relative to the object receiving the message to the given value. The default implementation of setValue:forKey: automatically unwraps NSNumber and NSValue objects that represent scalars and structs and assigns them to the property. See Representing Non-Object Values for details on the wrapping and unwrapping semantics. If the specified key corresponds to a property that the object receiving the setter call does not have, the object sends itself a setValue:forUndefinedKey: message. The default implementation of setValue:forUndefinedKey: raises an NSUndefinedKeyException. However, subclasses may override this method to handle the request in a custom manner.
setValue:forKey:
: 将该消息接收者的指定 key 的值设置为给定值。默认实现会自动把表示标量
和结构体
的 NSNumber 和 NSValue 对象解包然后赋值给属性。如果指定 key 所对应的属性没有对应的 setter
实现,则该对象将向自身发送 setValue:forUndefinedKey:
消息,valueForUndefinedKey:
的默认实现会抛出一个 NSUndefinedKeyException
的异常。但是子类可以重写此方法以自定义方式处理请求。Example:
AKPerson *person = [[AKPerson alloc] init]; [person setValue:@"akironer" forKey:@"name"]; NSLog(@"%@", [person valueForKey:@"name"]); 打印输出:akironer 复制代码
valueForKeyPath:
- Returns the value for the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key—that is, for which the default implementation of valueForKey: cannot find an accessor method—receives a valueForUndefinedKey: message.
valueForKeyPath:
: 返回于接受者的指定key path
上的值。key path
路径序列中不符合特定键的键值编码的任何对象,都会接收到 valueForUndefinedKey:
消息。
setValue:forKeyPath:
- Sets the given value at the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key receives a setValue:forUndefinedKey: message.
setValue:forKeyPath:
: 将该消息接收者的指定 key path
的值设置为给定值。key path
路径序列中不符合特定键的键值编码的任何对象都将收到setValue:forUndefinedKey:
消息Example:
AKTeacher *teacher = [[AKTeacher alloc] init]; teacher.subject = @"iOS"; person.teacher = teacher; [person setValue:@"iOS进阶之路" forKeyPath:@"teacher.subject"]; NSLog(@"%@",[person valueForKeyPath:@"teacher.subject"]); 打印输出:iOS进阶之路 复制代码
-> dictionaryWithValuesForKeys:
- Returns the values for an array of keys relative to the receiver. The method calls valueForKey: for each key in the array. The returned NSDictionary contains values for all the keys in the array.
key
数组的值。该方法会为数组中的每个 key
调用valueForKey:
。 返回的 NSDictionary
包含数组中所有键的值。
setValuesForKeysWithDictionary:
- Sets the properties of the receiver with the values in the specified dictionary, using the dictionary keys to identify the properties. The default implementation invokes setValue:forKey: for each key-value pair, substituting nil for NSNull objects as required.
setValuesForKeysWithDictionary:
:使用字典键标识属性,将指定字典中的对应值设置成该消息接收者的属性值。默认实现会对每一个键值对调用 setValue:forKey:
。设置时需要将 nil
替换成 NSNull
Collection objects, such as NSArray, NSSet, and NSDictionary, can’t contain nil as a value. Instead, you represent nil values using the NSNull object.
NSArray
NSSet
和 NSDictionary
等集合对象不能包含 nil
作为值, 可以使用 NSNull对象
代替 nil
值。[person setValuesForKeysWithDictionary:@{@"name": @"akironer", @"age": @(18)}, @"hobby":[NSNULL null]]; NSLog(@"%@", [person dictionaryWithValuesForKeys:@[@"name", @"age"]]); 打印输出: { age = 18; name = akironer; hobby = null; } 复制代码
// 方法一:普通方式 person.array = @[@"1",@"2",@"3"]; NSArray *array = [person valueForKey:@"array"]; // 不可不数组无法直接修改,用 array 的值创建一个新的数组 array = @[@"100",@"2",@"3"]; [person setValue:array forKey:@"array"]; NSLog(@"方法一:%@",[person valueForKey:@"array"]); // 方法二:KVC 的方式 NSMutableArray *ma = [person mutableArrayValueForKey:@"array"]; ma[0] = @"100"; NSLog(@"方法二:%@",[person valueForKey:@"array"]); 打印输出: 方法一:( 100, 2, 3 ) 方法二:( 100, 2, ) 复制代码
操作集合对象内部的元素来说,更高效的方式是使用 KVC 提供的可变代理方法
。KVC 为我们提供了三种不同的可变代理方法:
mutableArrayValueForKey:
& mutableArrayValueForKeyPath:
:返回的代理对象表现为一个 NSMutableArray 对象mutableSetValueForKey:
& mutableSetValueForKeyPath:
:返回的代理对象表现为一个 NSMutableSet 对象mutableOrderedSetValueForKey:
& mutableOrderedSetValueForKeyPath:
:返回的代理对象表现为一个 NSMutableOrderedSet 对象在使用 valueForKeyPath:
的时候,可以使用集合运算符来实现一些高效的运算操作。
@avg
: 返回操作对象指定属性的平均值@count
: 返回操作对象指定属性的个数@max
: 返回操作对象指定属性的最大值@min
: 返回操作对象指定属性的最小值@sum
: 返回操作对象指定属性值之和@distinctUnionOfObjects
: 返回操作对象指定属性的集合--去重@unionOfObjects
: 返回操作对象指定属性的集合@distinctUnionOfArrays
: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSArray@unionOfArrays
: 返回操作对象(集合)指定属性的集合@distinctUnionOfSets
: 返回操作对象(嵌套集合)指定属性的集合--去重,返回的是 NSSet非对象属性分为两类:
常用的基本数据类型需要在设置属性的时候包装成 NSNumber
对象
除了 NSPoint
NSRange
NSRect
和 NSSize
,对于自定义的结构体,也需要进行 NSValue
的转换操作.
typedef struct { float x, y, z; } ThreeFloats; // 设值 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); // 取值 ThreeFloats result; [reslut getValue:&result] ; NSLog(@"%f - %f - %f",result.x, result.y, result.z); 打印输出: {length = 12, bytes = 0x0000803f0000004000004040} 1.000000 - 2.000000 - 3.000000 复制代码
在学习KVC的搜索规则前,要先弄明白一个属性的作用,这个属性在搜索过程中起到很重要的作用。这个属性表示是否允许读取实例变量的值,如果为YES则在KVC查找的过程中,从内存中读取属性实例变量的值。
@property (class, readonly) BOOL accessInstanceVariablesDirectly; 复制代码
valueForKey:
方法的默认实现:valueForKey:
方法会在调用者传入 key
之后会在对象中按下列的步骤进行模式搜索:
get<Key>
<key>
is<Key>
以及 _<key>
的顺序查找对象中是否有对应的方法。简单getter方法
方法,则查找是否有 countOf<Key> 方法
objectIn<Key>AtIndex: 方法
(对应于 NSArray类
定义的原始方法) 以及 <key>AtIndexes: 方法
(对应于 NSArray 方法 objectsAtIndexes:)countOf<Key>
),再找到其他两个中的至少一个,则创建一个响应所有 NSArray
方法的代理集合对象,并返回该对象。(翻译过来就是要么是 countOf<Key> + objectIn<Key>AtIndex:
,要么是countOf<Key> + <key>AtIndexes:
,要么是 countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:
)简单NSArray方法
,查找名为 countOf<Key>
enumeratorOf<Key>
memberOf<Key>
这三个方法(对应于NSSet类
定义的原始方法)NSSet
方法的代理集合对象,并返回该对象accessInstanceVariablesDirectly
结果_<key>
_is<Key>
<key>
is<Key>
的顺序查找成员变量。如果找到了,将成员变量带上跳转到第 5 步,如果没有找到则跳转到第 6 步valueForUndefinedKey:
, 默认情况下抛出NSUndefinedKeyException
异常,但是继承于NSObject的子类可以重写该方法避免崩溃并做相应措施set<Key>:
_set<Key>
顺序查找对象中是否有对应的方法accessInstanceVariablesDirectly
结果_<key>
_is<Key>
<key>
is<Key>
的顺序查找成员变量,找到了就赋值;找不到就跳转第3步setValue:forUndefinedKey:
。默认情况下抛出NSUndefinedKeyException
异常,但是继承于NSObject的子类可以重写该方法避免崩溃并做出相应措施这里再明确下实例变量、成员变量、属性之间的区别:
我们不去重写属性的 getter 和 setter 方法以及声明对应的实例变量,那么编译器就会帮我们做这件事,那么是不是说有多少个属性,就会生成多少个对应的 getter 和 setter 呢?
显然,编译器不会这么傻。编译器在objc-accessors.mm
中运用通用原则
给所有属性都提供了同一的入口,setter方法会根据修饰符不同调用不同方法,最后统一调用reallySetProperty
方法示。
KVC在iOS开发中是绝不可少的利器,也是许多iOS开发黑魔法的基础。列举一下KVC的使用场景。
最基本的用法,相信大家都很属性了
对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的。
运用了KVC和Objc的runtime组合的技巧,完成模型和字典的相互转换
在 iOS 13 之前,我们可以通过 KVC 去获取和设置系统的私有属性,但从 iOS 13 之后,这种方式被禁用掉了。相信不少同学适配 iOS 13的时候,已经遇到了KVC的访问限制问题。
例如UITextField中的placeHolderText已经不能修改了,这里提供两种简答的修改思路,想要深入了解的可以参考关于iOS 13 中KVC 访问限制的一些处理
attributedPlaceholder
属性修改Placeholder
颜色NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:@"请输入占位文字" attributes: @{NSForegroundColorAttributeName:[UIColor redColor], NSFontAttributeName:textField.font }]; textField.attributedPlaceholder = attrString; 复制代码
UITextField
重新写一个方法- (void)resetTextField: (UITextField *)textField { Ivar ivar = class_getInstanceVariable([textField class], "_placeholderLabel"); UILabel *placeholderLabel = object_getIvar(textField, ivar); placeholderLabel.text = title; placeholderLabel.textColor = color; placeholderLabel.font = [UIFont systemFontOfSize:fontSize]; placeholderLabel.textAlignment = alignment; } 复制代码
在设值时设置空值,可以通过重写setNilValueForKey
来监听
In the default implementation, when you attempt to set a non-object property to a nil value, the key-value coding compliant object sends itself a setNilValueForKey: message. The default implementation of setNilValueForKey: raises an NSInvalidArgumentException, but an object may override this behavior to substitute a default value or a marker value instead, as described in Handling Non-Object Values.
在默认实现中,当您试图将非对象属性
设置为nil
时,KVC的对象会向自己发送一条setNilValueForKey:
消息。setNilValueForKey
的默认实现会引发NSInvalidArgumentException,但对象可以重写此行为以替换默认值或标记值。
Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism.
大体意思就是说:只对NSNumber
或者NSValue
类型的数据赋空值时,setNilValueForKey
才会触发。下面的例子中,subject不会触发
@implementation LGPerson - (void)setNilValueForKey:(NSString *)key { if ([key isEqualToString:@"age"]) { NSLog(@"你傻不傻: 设置 %@ 是空值",key); return 0; } [super setNilValueForKey:key]; } @ end [person setValue:nil forKey:@"age"]; [person setValue:nil forKey:@"subject"]; // subject不触发 - 官方注释里面说只对 NSNumber - NSValue 复制代码
对于未定义的key, 可以通过重写setValue:forUndefinedKey:
、valueForUndefinedKey:
来监听。
例如:
我们在字典转模型的时候,例如服务器返回一个id字段,但是对于客户端来说id是系统保留字段,可以重写setValue:forUndefinedKey:
方法并在内部处理id参数的赋值。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key { if ([key isEqualToString:@"id"]) { self.userId = [value integerValue]; } } 复制代码
在调用KVC时可以先进行验证,验证通过下面两个方法进行,支持key
和keyPath
两种方式。
验证方法需要我们手动调用,并不会在进行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; 复制代码
该方法的工作原理:
下面是使用验证方法的例子。在validateValue
方法的内部实现中,如果传入的value或key有问题,可以通过返回NO来表示错误,并设置NSError对象。
AKPerson* person = [[AKPerson alloc] init]; NSError* error; NSString* name = @"John"; if (![person validateValue:&name forKey:@"name" error:&error]) { NSLog(@"%@",error); } 复制代码
NSKeyValueCoding
隐式协议所提供的机制。valueForKey:
和 valueForKeyPath:
来取值,不考虑集合类型的话具体的取值过程如下:get<Key>
<key>
is<Key>
_<key>
的顺序查找方法accessInstanceVariablesDirectly
判断是否能读取成员变量来返回属性值_<key>
_is<Key>
<key>
is<Key>
的顺序查找成员变量setValueForKey:
和 setValueForKeyPath:
来取值,不考虑集合类型的话具体的设置值过程如下:set<Key>
_set<Key>
的顺序查找方法_<key>
_is<Key>
<key>
is<Key>
的顺序查找成员变量这次我们依据苹果的官方文档完成了KVC的探索,其实苹果的英文注释和官方文档写的非常用心,我们在探索 iOS 底层的时候,文档思维十分重要,多阅读文档总会有新的收获。