iOS-KVO
简介
先来看看官方的定义:
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
,全称是Key Value Coding
,即键值编码,它是通过NSKeyValueCoding
这个非正式协议启用的一种机制,遵循这个协议的对象提供了一种对属性的间接访问方式。当对象符合键值编码时,可以通过简洁,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。
通俗的说,使用KVC
,就可以直接通过字符串形式的key
值对对象的属性进行存取操作,而不需通过调用明确的存取方法。这样就可以在运行时动态在访问和修改对象的属性,而不是在编译时确定。
KVC
是基于Objective-C
的动态特性和Runtime
机制。
作用
使用KVC
可以实现以下的操作:
-
Access object properties
访问对象属性
-
-
Manipulate collection properties
操作集合属性
-
-
Invoke collection operators on collection objects
在集合对象上调用集合运算符
-
-
Access non-object properties
访问非对象属性
-
-
Access properties by key path
通过键路径访问属性
-
依赖KVO
的技术:
Key-value observing
Cocoa bindings
Core Data
AppleScript
KVC
的API
:
访问属性的API
// setter
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
// getter
- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
访问集合的API
:
-
mutableArrayValueForKey:
和mutableArrayValueForKeyPath:
这些返回行为像NSMutableArray
对象的代理对象。 -
mutableSetValueForKey:
和mutableSetValueForKeyPath:
这些返回行为像NSMutableSet
对象的代理对象。 -
mutableOrderedSetValueForKey:
和mutableOrderedSetValueForKeyPath:
这些返回行为像NSMutableOrderedSet
对象的代理对象。
以下就是一个KVC
的例子:
typedef struct {
float x, y, z;
} ThreeFloats;
@interface TPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, strong) NSArray *hobbies;
@property (nonatomic) ThreeFloats threeFloats;
@end
#import "TPerson.h"
@implementation TPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TPerson *person = [[TPerson alloc] init];
}
return 0;
}
2020-02-22 11:37:13.611079+0800 objc-debug[59128:2222986]
KVO
的使用
1. 访问对象属性
该协议指定了方法,例如通用getter valueForKey:
和通用setter setValue:forKey:
,用于通过名称或键(参数化为字符串)来访问对象属性。这些方法和相关方法的默认实现使用键来定位基础数据并与基础数据进行交互。
1.1 直接访问
我们可以直接使用点语法访问对象的属性。
person.age = 12;
NSLog(@"==age==%d==", person.age);
// ==age==12==
1.2 间接访问
[person setValue:@"AAA" forKey:@"name"];
NSLog(@"==name==%@==", [person valueForKey:@"name"]);
// ==name==AAA==
2. 操作集合属性
NSArray
就像其他任何属性一样,访问方法的默认实现与对象的集合属性(如对象)一起使用。此外,如果对象为属性定义了集合访问器方法,则它将启用对集合内容的键值访问。这通常比直接访问更有效,并且允许您通过标准化接口使用自定义集合对象。
person.hobbies = @[@"iOS", @"swift"];
person.hobbies[0] = @"android"; // 这行代码是错误的
2.1 整体赋值即可
NSArray *array = person.hobbies;
array = @[@"android", @"swift"];
person.hobbies = array;
NSLog(@"==hobbies==%@==", person.hobbies);
// ==hobbies==(android,swift)==
2.2 使用可变数组
NSMutableArray *mArray = [person mutableArrayValueForKey:@"hobbies"];
mArray[0] = @"android";
NSLog(@"==hobbies==%@==", person.hobbies);
// ==hobbies==(android,swift)==
同样,我们也可以直接对字典进行操作:
NSDictionary *setDict = @{@"name": @"BBB",
@"age": @12,
@"hobbies": @[@"iOS", @"swift"] };
// 字典转模型
[person setValuesForKeysWithDictionary:setDict];
NSLog(@"==%@==",person);
// 键数组转模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *getDict = [person dictionaryWithValuesForKeys:array];
NSLog(@"==%@==", getDict);
3. 在集合对象上调用集合运算符
向键值编码兼容对象valueForKeyPath:
发送消息时,可以在键路径中嵌入集合运算符。集合运算符是一小段关键字之一,其后跟一个at
符号(@)
,该符号指定getter
在返回数据之前应以某种方式操作数据。valueForKeyPath:
提供者的默认实现NSObject
此行为。
常见的运算符有@avg、@count、@max、@min、@sum
该例中我们向数组发送了length
的方法,返回了每个元素的长度,发送lowercaseString
方法返回了每个元素的小写。
NSArray *array = person.hobbies;
NSArray *lenStr= [array valueForKeyPath:@"length"];
NSLog(@"%@",lenStr); // (3,5)
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"%@",lowStr); // (ios,swift)
这个例子中,我们使用集合运算符对对象age
这个属性进行了操作:
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
TPerson *person = [[TPerson alloc] init];
NSDictionary* dict = @{ @"name": @"CCC",
@"age": @(12+i) };
[person setValuesForKeysWithDictionary:dict];
[personArray addObject:person];
}
NSLog(@"%@", [personArray valueForKey:@"age"]);
float avg = [[personArray valueForKeyPath:@"@avg.age"] floatValue];
NSLog(@"%f", avg);
int count = [[personArray valueForKeyPath:@"@count.age"] intValue];
NSLog(@"%d", count);
int sum = [[personArray valueForKeyPath:@"@sum.age"] intValue];
NSLog(@"%d", sum);
int max = [[personArray valueForKeyPath:@"@max.age"] intValue];
NSLog(@"%d", max);
int min = [[personArray valueForKeyPath:@"@min.age"] intValue];
NSLog(@"%d", min);
控制台输出如下:
2020-02-22 17:48:28.799535+0800 objc-debug[60596:2386404] (12, 13, 14, 15, 16, 17)
2020-02-22 17:48:28.802529+0800 objc-debug[60596:2386404] 14.500000
2020-02-22 17:48:28.803014+0800 objc-debug[60596:2386404] 6
2020-02-22 17:48:28.803556+0800 objc-debug[60596:2386404] 87
2020-02-22 17:48:28.803871+0800 objc-debug[60596:2386404] 17
2020-02-22 17:48:28.804161+0800 objc-debug[60596:2386404] 12
4. 访问非对象属性
协议的默认实现检测非对象属性(包括简单类型和结构体),并自动将它们转化和解包为对象,以在协议接口上使用。另外,该协议声明了一种方法,该方法兼容对象针对nil
通过键值编码接口在非对象属性上设置值的情况提供适当的操作。
4.1 简单类型转为NSNumber
4.2 结构体转为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==%@==",reslut);
// ==reslut=={length = 12, bytes = 0x0000803f0000004000004040}==
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
// 1.000000 - 2.000000 - 3.000000
5. 通过键路径访问属性
当您具有与键值编码兼容的对象的层次结构时,也就是模型套模型,可以使用基于键路径的方法来获取或设置值。
创建一个类TStudent
@interface TStudent : NSObject
@property (nonatomic, assign) int gender;
@end
然后给TPerson
添加一个属性:
@property (nonatomic, strong) TStudent *student;
TStudent *student = [[TStudent alloc] init];
person.student = student;
[person setValue:@0 forKeyPath:@"student.gender"];
KVC
原理
设置值的过程
setValue:forKey:
的默认实现是,使用给定键和值参数作为输入,将名为key
的属性设置为value
,设置的时候按照以下的顺序:
-
首先查找第一个名为
set<Key>:
或者_set<Key>
的访问器。如果找到,则调用方法设置并完成。 -
如果没有找到简单的访问,且类方法
accessInstanceVariablesDirectly
返回的是YES
,那就按照这个顺序去寻找一个实例变量名称类似_<key>
,_is<Key>
,<key>
,is<Key>
。如果找到,直接设置值并完成操作。 -
在找不到访问器或实例变量后,调用
setValue:forUndefinedKey:
。默认情况下会引发异常,但是NSObject
的子类可能提供特定的行为。
获取值的过程
valueForKey:
的默认实现是给定一个key
参数作为输入,它从接收该valueForKey:
调用的类实例内部执行以下过程:
-
按照
get<Key>
,<key>
,is<Key>
,_<key>
的顺序查找对象中是否有对应的方法。- 如果找到了,将方法返回值带上执行步骤5
- 如果没有找到,执行步骤2
-
在实例中搜索名称类似于
countOf<Key>
和objectIn<Key>AtIndex:
这种模式的方法(对应于NSArray
类定义的原始方法)以及<key>AtIndexes:
方法(对应于NSArray
方法objectsAtIndexes:
)。- 如果找到
countOf<Key>
,而且还找到objectIn<Key>AtIndex:
、<key>AtIndexes:
、objectsAtIndexes:
中的至少一个,则创建一个响应所有NSArray
方法的集合代理对象并将其返回 - 如果没有找到,执行步骤3
- 如果找到
-
查找名为
countOf<Key>
,enumeratorOf<Key>
和memberOf<Key>
这三个方法(对应于NSSet
类定义的原始方法)- 如果找到这三个方法,则创建一个响应所有
NSSet
方法的代理集合对象,并返回该对象 - 如果没有找到,执行步骤4
- 如果找到这三个方法,则创建一个响应所有
-
判断类方法
accessInstanceVariablesDirectly
结果,- 如果是
YES
,则按照_<key>
,_is<Key>
,<key>
,is<Key>
的顺序查找实例变量,- 如果找到,带上变量执行步骤5
- 如果找不到,执行步骤6
- 如果是
NO
,执行步骤6
- 如果是
-
如果属性值是对象指针,直接返回;如果属性值可以转化为
NSNumber
类型,则将其转化为NSNumber
类型返回;如果属性值也不能转化为NSNumber
类型,则将其转化为NSValue
类型返回。 -
调用
valueForUndefinedKey:
,默认情况下,这会引发一个异常,但是NSObject
的子类可能提供特定的行为。
下面我们来验证一下这个过程,由于属性会自动生成setter/getter
,所以我们使用成员变量进行验证。给TPerson
添加以下实例变量、属性、方法:
@interface TPerson : NSObject{
@public
// 按照`_<key>`,`_is<Key>`,`<key>`,`is<Key>`
NSString *_name;
NSString *_isName;
NSString *name;
NSString *isName;
}
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet *set;
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
/*********设值*******/
// 按照`set<Key>:`、`_set<Key>`的顺序
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
/*********取值*******/
// 按照`get<Key>`, `<key>`, `is<Key>`,`_<key>`
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
/*********设值*******/
[person setValue:@"DDD" forKey:@"name"];
NSLog(@"-_name-%@-_isName-%@-name-%@-isName-%@",person->_name,person->_isName,person->name,person->isName);
NSLog(@"-_isName-%@-name-%@-isName-%@",person->_isName,person->name,person->isName);
NSLog(@"-name-%@-isName-%@",person->name,person->isName);
NSLog(@"isName-%@",person->isName);
/*********取值*******/
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
当我们执行[person setValue:@"DDD" forKey:@"name"]
,控制台会输出:
-[TPerson setName:] - DDD
这正好验证了第一步的第一个方法,我们再注释setName
方法,运行程序,控制台输出:
-[TPerson _setName:] - DDD
注释掉setName
和_setName
方法,运行程序,控制台输出:
-_name-DDD-_isName-(null)-name-(null)-isName-(null)
我们在注释掉NSString *_name;
和第一个打印方法,运行程序,控制台输出:
-_isName-DDD-name-(null)-isName-(null)
后面几步同理。可以看出,设值顺序和我们之前的结论完全一致。接着我们来验证取值顺序,注释掉设值的相关代码,运行程序,控制台输出:
取值:getName
注释掉getName
方法,运行程序,控制台输出:
取值:name
剩下的验证就不在此处做赘述。可以得出,取值顺序也和结论一致。
自定义KVC
既然我们了解了KVC
的取值、设值原理和过程,那么我们可以简单的模拟一下其setValue:forKey:
和valueForKey:
的过程,暂时忽略NSArray、NSSet
的处理以及NSNumber
和NSValue
的相互转换过程。
思路
自定义设值方法: (setValue:forKey:)
- 对传入的
key
做非空判断
- 对传入的
- 如果有相关的
set<Key>、 _set<Key>
方法,调用之后返回
- 如果有相关的
-
- 对
accessInstanceVariablesDirectly
方法的返回值进行判断
- 返回YES,执行步骤4
- 返回NO,抛出异常
- 对
-
- 获取到所有的变量,然后按照
_<key>、_is<Key>、<key>、 is<Key>
的顺序来判断获取的变量是否包含有这些key
,
- 如果有,使用
object_setIvar
赋值,然后返回 - 如果没有,抛出异常
- 获取到所有的变量,然后按照
自定义取值方法: (valueForKey)
- 对传入的
key
做非空判断
- 对传入的
- 如果有相关的
get<Key>、<key>、countOf<Key>、 objectIn<Key>AtIndex
方法,调用,返回
- 如果有相关的
-
- 对
accessInstanceVariablesDirectly
方法的返回值进行判断
- 返回YES,执行步骤4
- 返回NO,抛出异常
- 对
-
- 获取到所有的变量,然后按照
_<key>、_is<Key>、<key>、 is<Key>
的顺序来判断获取的变量是否包含有这些key
,
- 如果有,使用
object_getIvar
取值,然后返回 - 如果没有,抛出异常
- 获取到所有的变量,然后按照
实现
- (void)customSetValue:(nullable id)value forKey:(NSString *)key{
// 1:非空判断一下
if (key == nil || key.length == 0) return;
// 2:找到相关方法 set<Key> _set<Key> setIs<Key>
// key 要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
if ([self customPerformSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self customPerformSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"CustomUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
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]) {
// 4.2 获取相应的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 对相应的 ivar 设置值
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
// 5:如果找不到相关实例
@throw [NSException exceptionWithName:@"CustomUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
- (nullable id)customValueForKey:(NSString *)key{
// 1:key 判断非空
if (key == nil || key.length == 0) {
return nil;
}
// 2:找到相关方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex
// key 要大写
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 = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
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:@"CustomUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// _name -> _isName -> name -> isName
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
return @"";
}
#pragma mark - 相关方法
- (BOOL)customPerformSelectorWithMethodName:(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;
}
- (id)performSelectorWithMethodName:(NSString *)methodName{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
}
return nil;
}
- (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;
}
总结
KVC
是基于NSKeyValueCoding
协议,核心是setValue:forKey:
和valueForKey:
。其原理如下:
Tips setNilValueForKey
当我们对非对象的属性传入nil
的时候,程序会崩溃,告诉我们不能对其设置nil
,这是因为在底层调用了以下方法:
- (void)setNilValueForKey:(NSString *)key
如果我们实现了这个方法,则就不会崩溃。
而当给对象的属性传入nil
的时候,底层会判断直接会直接不响应也不会崩溃。setNilValueForKey
只对可以转为NSNumber
或者NSValue
的非对象属性响应、
参考文献: Apple
官方文档