iOS底层探索 -- KVC 底层原理分析
iOS底层探索 -- KVC 底层原理分析
前言
在日常的开发中,在对数据进行处理中,常常使用三方框架将其转换为模型 (model)
,以方便使用点语法
进行调用。这些框架底层都是运用的KVC(Key-Value Coding)
,今天来探索一下KVC
底层的原理。
1. KVC(Key-Value Coding)初探
KVC
即Key-Value Coding
,翻译过来就是键值编码
,关于这个概念,具体可以查看Apple
的官方文档
Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties
.【译】键值编码是由
NSKeyValueCoding
非正式协议启用的一种机制,对象
采用这种机制来提供对其属性的间接访问。
当对象符合键值编码时,可以通过简洁,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。
当对象继承自NSObject
时,可以通过简洁的方法(setValue:forKey
和valueForKey
)实现以下操作:
- 访问对象属性,例如通过
getter valueForKey:
和setter setValue:forKey:
,用于通过名称或键(参数化为字符串)来访问对象属性。 - 操作集合属性,比如:
NSArray
- 在集合对象上调用集合运算符
- 访问非对象属性,比如:
结构体
- 通过键路径访问属性
2. KVC 深入
通常在我们给对象声明属性时,会有一下几类:
-
属性。这些是简单的值,例如标量,字符串或布尔值。值对象(例如)
NSNumber
和其他不可变类型(例如)NSColor
也被视为属性。 -
一对一的关系。这些是具有自己属性的
可变对象
。对象
的属性可以更改,而无需更改对象本身。例如,银行帐户对象可能具有所有者属性,该属性是Person对象的实例,该对象本身具有地址属性。所 有者的地址可以更改,而无需更改银行帐户持有的所有者
-
一对多关系。这些是集合对象。比如:
NSArray
和NSSet
2.1 访问对象属性
- 通过
valueForKey:
和setValue:ForKey:
来间接的获取和设置属性值
[person setValue:@"KC" forKey:@"name"];
[person setValue:@19 forKey:@"age"];
[person setValue:@"酷C" forKey:@"myName"];
NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);
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:
自动实现代表标量和结构的自动包装NSNumber
和NSValue
对象的默认实现,并将其分配给属性。有关包装和展开语义的详细信息,请参见表示非对象值。
如果指定的键对应于接收setter调用的对象所不具有的属性,则该对象向自身发送setValue:forUndefinedKey:
消息。的默认实现setValue:forUndefinedKey:
会引发NSUndefinedKeyException。
但是,子类可以重写此方法以自定义方式处理请求。
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.
【译】返回由
key
参数命名的属性的值。如果根据访问者搜索模式中描述的规则找不到由键命名的属性,则该对象向自身发送一条valueForUndefinedKey:
消息。的默认实现valueForUndefinedKey:
会引发一个NSUndefinedKeyException
,但是子类可以覆盖此行为,并更优雅地处理该情况。
-
valueForKeyPath:
和setValue:ForKeyPath:
在Storyboard
或xib
中使用KVC
在Storyboard
或 xib
中使用KeyPath
来修改控件的某个属性,如下图:
对View
的layer.cornerRadius
进行修改,实现圆角,其实本质就是调用valueForKeyPath:
和 setValue:ForKeyPath:
开发中不建议使用这种方式在 Storyboard 或者 xib 中修改,会造成后人很难找到很难维护
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
: 返回相对于接收者的指定**路径的值。**路径序列中不符合特定键的键值编码的任何对象(即,默认实现valueForKey:
无法找到访问器方法)均会接收到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
:在相对于接收器的指定键路径处设置给定值。**路径序列中不符合特定键的键值编码的任何对象都会收到一条setValue:forUndefinedKey:
消息。
通过valueForKeyPath
和setValue:forKeyPath
,可以对具有自己数据的可变对象设值,如下示例:
LGPerson *person = [[LGPerson alloc] init];
LGStudent *student = [[LGStudent alloc] init];
student.subject = @"iOSer";
person.student = student;
[person setValue:@"我最帅" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
-
dictionaryWithValuesForKeys:
和setValuesForKeysWithDictionary:
查看官方文档
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.
【译】返回相对于接收者的键数组的值。该方法调用
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.
【译】使用字典键识别属性,以指定字典中的值设置接收器的属性。默认实现
setValue:forKey:
为每个键值对调用,设置时将nil
替换为NSNull
。
NSDictionary* dict = @{
@"name":@"CC",
@"nick":@"KC",
@"subject":@"iOS",
@"age":@18,
@"length":@180
};
LGStudent *p = [[LGStudent alloc] init];
// 字典转模型
[p setValuesForKeysWithDictionary:dict];
NSLog(@"%@",p);
// 键数组转模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
NSLog(@"%@",dic);
需要注意的是:集合对象,如
NSArray
,NSSet
和NSDictionary
,不能包含nil
的值。而是nil
使用NSNull
对象表示值。NSNull
提供一个代表nil
对象属性值的实例。
实现dictionaryWithValuesForKeys:
和setValuesForKeysWithDictionary:
会自动在NSNull
(在dictionary
参数中)和nil
(在存储属性中)之间相互转换。
2.2 访问集合属性
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"100";
NSLog(@"%@",[person valueForKey:@"array"]);
2.3 集合运算符
在使用valueForKeyPath:
发送消息时,可以在键路径中嵌入集合运算符
路径格式:
1.left key path:指向的要进行运算的集合,如果是直接给集合发送的 valueForKeyPath: 消息,left
key path 可以省略
2.right key path:指定了运算符应在集合中进行操作的属性。除之外,所有集合运算符都@count需要正确的**路径
集合运算符可以分为三种:
-
聚合运算符
-
@avg
将指定的属性将其转换为double(用0代替nil值)
,并计算这些值的算术平均值。然后,以NSNumber
形式返回
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
-
@count
返回操作对象指定属性的个数
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
-
@max
返回指定属性的最大值
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
-
@min
返回指定属性的最小值
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
-
@sum
返回指定属性的之和
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
-
-
数组运算符
-
@distinctUnionOfObjects
返回操作对象指定属性的集合–去重
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
-
@unionOfObjects
返回操作对象指定的属性的集合
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
-
-
嵌套运算符
-
@distinctUnionOfArrays
返回一个数组
,该数组包含操作对象(嵌套集合)指定属性–去重 -
@unionOfArrays
返回一个数组
,该数组包含操作对象指定的属性,不去重。 -
@distinctUnionOfSets
交集 返回一个NSSet
,该NSSet
包含操作对象(嵌套集合)指定属性–去重
-
2.4 访问非对象属性
非对像属性包含两种形式:基本数据类型,结构体(struct
)
- 访问基本数据类型
如图:常用的基本数据类型需要在设置属性的时候包装成 NSNumber
类型,然后在读取值的时候使用各自对应的读取方法,
如: double 类型的标量读取的时候使用 doubleValue
- 结构体
如图所示:NSPoint
、NSRange
、NSRect
和NSSize
需要转换成 NSValue
类型,对于自定义的结构体,也需要进行 NSValue
的转换操作。如下示例:
typedef struct {
float x, y, z;
} ThreeFloats;
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
NSValue* result = [myClass valueForKey:@"threeFloats"];
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];
2.5 属性验证
调用validateValue:forKey:error:
方法或者alidateValue:forKeyPath:error:
方法进行属性验证。
会在接收验证消息的对象
中查找与之匹配的validate<Key>:error:
方法,如果没有这个方法,则验证成功,返回YES
。当存在特定于属性的验证方法时,默认实现将返回调用该方法的结果。
简单的说,就是防止对错误的key
赋值,也可以判断特定的key
,进行重定向为另一个key
。
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
if([inKey isEqualToString:@"name"]){
[self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:inKey];
return YES;
}
*outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的属性",inKey,self] code:10088 userInfo:nil];
return NO;
}
2.6 KVC 取值和赋值原理
-
赋值原理
- 依次判断是否有
set<Key>
或者_set<Key>
或者setIs<key>
的方法。如果找到,直接调用,没有,则进入第 2 步。 - 判断
accessInstanceVariablesDirectly
方法,是否开启实例变量赋值,若返回YES
,则进入步骤3,返回NO
,则进去步骤4。 - 依次寻找类似于
_<key>
、_is<Key>
、<key>
或者is<Key>
的实例变量。如果找到,直接设置变量。找不到,则进去步骤4。 - 找不到访问器或实例变量后,调用
setValue:forUndefinedKey:
,报错,引发异常。
- 依次判断是否有
-
取值原理
-
依次判断是否有访问方法
get<Key>
、<key>
、is<Key>
或者_<key>
,如果找到,则执行步骤 6进行细节处理。否则,请继续下一步。 -
判断是否是
NSArray
, -
判断是否是
NSSet
-
判断
accessInstanceVariablesDirectly
方法,是否开启实例变量赋值,若返回YES
,则进入步骤5,返回NO
,则进去步骤7 -
依次查找名为
_<key>
、_is<Key>
、<key>
或者is<Key>
的实例变量,如果找到,直接获取值,找不到则直接执行步骤 6。 -
如果检索到的属性值是对象指针,则只需返回结果,
如果该值是所支持的标量类型
NSNumber
,则将其存储在NSNumber
实例中并返回如果结果是
NSNumber
不支持的标量类型,请转换为NSValue
对象并返回该对象 -
调用
valueForUndefinedKey:
,报错,引发异常
-