详解Key-Value Coding源码
KVC源码阅读。
一、接口
1 | @interface NSObject(NSKeyValueCoding) |
集合代理对象
这里简单总结集合代理对象的使用。
当我们在对象上调用
-valueForKey:
的时候,它可以返回NSArray
,NSSet
或是NSOrderedSet
的集合代理对象。这个类没有实现通常的-<Key>
方法,但是它实现了代理对象所需要使用的很多方法。
NSArray
1 | @interface Primes : NSObject |
对于NSArray, 实现-countOf<Key>
方法,-objectIn<Key>AtIndex:
或者-<key>AtIndexes:
中的一个即可,当然如果再实现-get<Key>:range:
将会增强性能。
上面的例子中,key是“primes”,实际上并没有这个primes数组,而是用了一个C数组代理了。@property (nonatomic, copy, readonly) NSArray *primes;
和@dynamic primes;
这两句话可以省略(下文的例子就省略了),这里加上的原因是,便于外界知晓具体的key值。
使用:
1 | Primes *primes = [Primes new]; |
可见,获得的对象并不是一个NSArray,而是NSKeyValueArray
。
NSSet
1 | @interface PrimesSet : NSObject |
对于NSSet,要实现-countOf<Key>
、-enumeratorOf<Key>
和-memberOf<Key>:
这三个方法。
使用:
1 | PrimesSet *primesSet = [PrimesSet new]; |
获取到的对象是NSKeyValueSet
。
NSOrderedSet
1 | @interface PrimesOrderedSet : NSObject |
必须实现的方法是-countOf<Key>
和-indexIn<Key>OfObject:
。二选一实现的方法是-objectIn<Key>AtIndex:
和-<key>AtIndexes:
。如果再实现-get<Key>:range:
将会增强性能。
使用:
1 | PrimesOrderedSet *primesOrderedSet = [PrimesOrderedSet new]; |
获取到的对象是NSKeyValueOrderedSet
。
集合操作
数组最大值:
1 | NSArray *array = |
模型数组最大值:
1 | NSArray *array = @[person1, person2, person3]; |
其他操作符:
1 | @max @min: 获得数组中最大(或者最小)的一个元素 |
分类的KVC
一般的场景是这样:
1 | // 主类 |
对于主类中没有的key,分类实现特定的方法后,KVC也将会生效。如果是取值,分类必须实现这样的方法:getPrimitive<key>
或者primitive<key>
。上面的例子中,key是volume。如果是设值,分类必须实现setPrimitive<key>:
方法。
使用:
1 | Portion *p = [Portion new]; |
当然,本质上来讲,KVC并不介意这些方法在主类还是分类实现的,只要有实现就成。上面的只是一个例子,实际上,完全依靠主类也是无妨的。
1 | @interface Portion : NSObject |
二、取值
valueForKey:
苹果在接口这里已经给出了其基本原理:
The default implementation of this method does the following:
Searches the class of the receiver for an accessor method whose name matches the pattern
-get<Key>, -<key>, or -is<Key>
, in that order. If such a method is found it is invoked. If the type of the method’s result is an object pointer type the result is simply returned. If the type of the result is one of the scalar types supported by NSNumber conversion is done and an NSNumber is returned. Otherwise, conversion is done and an NSValue is returned (new in Mac OS 10.5: results of arbitrary type are converted to NSValues, not just NSPoint, NRange, NSRect, and NSSize).(introduced in Mac OS 10.7). Otherwise (no simple accessor method is found), searches the class of the receiver for methods whose names match the patterns
-countOf<Key> and -indexIn<Key>OfObject: and -objectIn<Key>AtIndex:
(corresponding to the primitive methods defined by the NSOrderedSet class) and also-<key>AtIndexes:
(corresponding to -[NSOrderedSet objectsAtIndexes:]). If a count method and an indexOf method and at least one of the other two possible methods are found, a collection proxy object that responds to all NSOrderedSet methods is returned. Each NSOrderedSet message sent to the collection proxy object will result in some combination of-countOf<Key>, -indexIn<Key>OfObject:, -objectIn<Key>AtIndex:, and -<key>AtIndexes:
messages being sent to the original receiver of-valueForKey:
. If the class of the receiver also implements an optional method whose name matches the pattern -get:range: that method will be used when appropriate for best performance.
Otherwise (no simple accessor method or set of ordered set access methods is found), searches the class of the receiver for methods whose names match the patterns
-countOf<Key> and -objectIn<Key>AtIndex:
(corresponding to the primitive methods defined by the NSArray class) and (introduced in Mac OS 10.4) also-<key>AtIndexes:
(corresponding to -[NSArray objectsAtIndexes:]). If a count method and at least one of the other two possible methods are found, a collection proxy object that responds to all NSArray methods is returned. Each NSArray message sent to the collection proxy object will result in some combination of-countOf<Key>, -objectIn<Key>AtIndex:, and -<key>AtIndexes:
messages being sent to the original receiver of-valueForKey:
. If the class of the receiver also implements an optional method whose name matches the pattern-get<Key>:range:
that method will be used when appropriate for best performance.(introduced in Mac OS 10.4). Otherwise (no simple accessor method or set of ordered set or array access methods is found), searches the class of the receiver for a threesome of methods whose names match the patterns
-countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>:
(corresponding to the primitive methods defined by the NSSet class). If all three such methods are found a collection proxy object that responds to all NSSet methods is returned. Each NSSet message sent to the collection proxy object will result in some combination of-countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>:
messages being sent to the original receiver of-valueForKey:
.Otherwise (no simple accessor method or set of collection access methods is found), if the receiver’s class’ +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern
_<key>, _is<Key>, <key>, or is<Key>
, in that order. If such an instance variable is found, the value of the instance variable in the receiver is returned, with the same sort of conversion to NSNumber or NSValue as in step 1.Otherwise (no simple accessor method, set of collection access methods, or instance variable is found), invokes
-valueForUndefinedKey:
and returns the result. The default implementation of-valueForUndefinedKey:
raises an NSUndefinedKeyException, but you can override it in your application.Compatibility notes:
- For backward binary compatibility, an accessor method whose name matches the pattern
-_get<Key>
, or-_<key>
is searched for between steps 1 and 3. If such a method is found it is invoked, with the same sort of conversion to NSNumber or NSValue as in step 1. KVC accessor methods whose names start with underscores were deprecated as of Mac OS 10.3 though.- The behavior described in step 5 is a change from Mac OS 10.2, in which the instance variable search order was
<key>, _<key>
.- For backward binary compatibility,
-handleQueryWithUnboundKey:
will be invoked instead of-valueForUndefinedKey:
in step 6, if the implementation of-handleQueryWithUnboundKey:
in the receiver’s class is not NSObject’s.
简单翻译如下:
- 按照
-get<Key>, -<key>, -is<Key>
的顺序搜索该类的存取器方法,若找到,则直接调用。如果方法调用的结果是id
类型,直接把结果返回。如果方法调用的结果是能够被NSNumber
转换的标量类型,则结果会被转为NSNumber
返回。否则对于一般的标量类型,这些类型将会被转化为NSValue
(在Mac OS 10.5及以后,不仅仅支持NSPoint, NRange, NSRect
,以及NSSize
这些类型)。 - 如果简单的存取器方法没有找到,那么搜索该类的
-countOf<Key>, -indexIn<Key>OfObject:
方法,还有-objectIn<Key>AtIndex:
(对应被NSOrderedSet
类所定义的方法),-<key>AtIndexes:
(对应-[NSOrderedSet objectsAtIndexes:]
)方法。如果-countOf<Key>, -indexIn<Key>OfObject:
这两个方法被找到,另外两个方法中的至少一个被找到,那么这个能响应NSOrderedSet
所有方法的集合代理对象会被返回。发送给原来消息接收者的-valueForKey:
消息,将会被这个集合代理对象的-countOf<Key>, -indexIn<Key>OfObject:, -objectIn<Key>AtIndex:, -<key>AtIndexes:
这些方法共同处理。如果这个代理对象也实现了可选的-get<Key>:range:
方法,这将有助于增强性能。 - 如果存取器方法和
ordered set
的代理方法没有被找到,那么搜索该类的-countOf<Key>
方法,还有-objectIn<Key>AtIndex:
(对应被NSArray
类所定义的方法),-<key>AtIndexes:
(对应-[NSArray objectsAtIndexes:]
)方法。如果-countOf<Key>
这个方法被找到,另外两个方法中的至少一个被找到,那么这个能响应NSArray
所有方法的集合代理对象会被返回。发送给原来消息接收者的-valueForKey:
消息,将会被这个集合代理对象的-countOf<Key>, -objectIn<Key>AtIndex:, -<key>AtIndexes:
这些方法共同处理。如果这个代理对象也实现了可选的-get<Key>:range:
方法,这将有助于增强性能。 - 如果存取器方法、
ordered set
和array
的代理方法都没有被找到,那么尝试搜索-countOf<Key>, -enumeratorOf<Key>, -memberOf<Key>:
这些(被NSSet
类所定义的)方法。如果这三个方法都能被找到,那么这个能响应NSSet
所有方法的集合代理对象会被返回。发送给原来消息接收者的-valueForKey:
消息,将会被这个集合代理对象的countOf<Key>, -enumeratorOf<Key>, -memberOf<Key>:
这些方法共同处理。 - 如果存取器方法、
ordered set
、array
以及set
的代理方法都没有被找到,倘若此时消息接收者的+accessInstanceVariablesDirectly
属性返回的是YES
(默认实现就是返回YES
),那么按照_<key>, _is<Key>, <key>, is<Key>
的顺序搜索该类的实例变量。如果找到这个实例变量,那么按照步骤1中的类型转换规则返回这个实例变量的值。 - 否则(啥也没找到),调用
-valueForUndefinedKey:
方法并返回结果。这个方法的默认实现是抛出NSUndefinedKeyException
异常,不过你可以重写该方法自行实现。
兼容性:
为了向后兼容,会在步骤1中查找名称为
-_get<Key>, -_<key>
的存取器方法。如果找到了,会进行调用并按照步骤1中的类型转换规则返回调用的结果。从Mac OS 10.2开始,步骤5中的实例变量搜索顺序从原先的
<key>, _<key>
改为现在的_<key>, _is<Key>, <key>, is<Key>
。如果
-handleQueryWithUnboundKey:
的实现不是NSObject
的默认实现(换句话说,自己手动实现了-handleQueryWithUnboundKey:
方法),那在步骤6中,-handleQueryWithUnboundKey:
方法将会代替-valueForUndefinedKey:
方法被调用。
说得清晰明了。流程图如下:
方法一
1 | - (id)valueForKey:(NSString *)key { |
这个方法主要做了四件事:
取值时,使用
OSSpinLockLock
保证线程安全根据class和key,生成一个
NSKeyValueGetter
对象,用于封装信息取值时,会根据class和key配置一个简单的
Getter
,首先到CFSet
缓存集合中进行查找,以提高查找速度当
key
不存在时,直接抛出异常: 参数有误
方法二
当缓存集合中不存在时,便进入了更为具体的“查找”流程中。
1 | // 详细的查找流程(此处假定key为"name") |
这个方法详细地设定了查找的顺序,值得关注的是,NSKeyValueMethodForPattern()
这个函数调用的次数相当的多。
方法三
1 | //最后一次查找 |
方法四
1 | // 转发处理给NSKeyValueUndefinedGetter对象 |
方法五
1 | // NSKeyValueUndefinedGetter负责调用其父类(NSKeyValueGetter)的构造方法 |
方法怎么查找
1 | // NSKeyValueMethodForPattern |
实例变量怎么查找
1 | // NSKeyValueIvarForPattern |
NSKeyValueMethodGetter如何创建
1. NSKeyValueMethodGetter构造方法中生成IMP
1 | @implementation NSKeyValueMethodGetter |
由于KVC返回的类型为对象(NSObject)
,所以需要对方法返回值类型分别进行判断从而为Getter
赋值不同的函数指针。也即从BOOL、double、int、CGSize
等普通类型转化为NSNumber、NSValue、id
等对象类型的函数(指针)。
2. 在父类NSKeyValueAccessor
中, 对class key selector imp method
参数 参数数量等信息进行保存
1 | @implementation NSKeyValueAccessor |
NSKeyValueIvarGetter如何创建
1 | - (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key containerIsa:(Class)containerIsa ivar:(Ivar)ivar { |
同样地,判断实例变量的类型编码,进而赋值不同的IMP。
怎么根据Getter取值
1. 线程校验
1 | void NSKeyValueObservingAssertRegistrationLockNotHeld() { |
2. 直接调用Getter中存储的方法实现(getter.implementation)
1 | id _NSGetUsingKeyValueGetter(id object, NSKeyValueGetter *getter) { |
valueForKeyPath:
假定这里的keyPath为@"key1.key2.key3.key4"
。
1 | - (id)valueForKeyPath:(NSString *)keyPath { |
这里就有一个问题了,相似的把keyPath拆分逻辑的逻辑为啥要写两个,一个转为C字符串拆分,一个直接拆分? 揣测两者的区别主要是对字符串编码的判断。
三、设值
设值的流程就比较简单了。
The default implementation of this method does the following:
Searches the class of the receiver for an accessor method whose name matches the pattern
-set<Key>:
. If such a method is found the type of its parameter is checked. If the parameter type is not an object pointer type but the value is nil-setNilValueForKey:
is invoked. The default implementation of-setNilValueForKey:
raises an NSInvalidArgumentException, but you can override it in your application. Otherwise, if the type of the method’s parameter is an object pointer type the method is simply invoked with the value as the argument. If the type of the method’s parameter is some other type the inverse of the NSNumber/NSValue conversion done by-valueForKey:
is performed before the method is invoked.Otherwise (no accessor method is found), if the receiver’s class’
+accessInstanceVariablesDirectly
property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern_<key>, _is<Key>, <key>, or is<Key>
, in that order. If such an instance variable is found and its type is an object pointer type the value is retained and the result is set in the instance variable, after the instance variable’s old value is first released. If the instance variable’s type is some other type its value is set after the same sort of conversion from NSNumber or NSValue as in step 1.Otherwise (no accessor method or instance variable is found), invokes
-setValue:forUndefinedKey:
. The default implementation of-setValue:forUndefinedKey:
raises an NSUndefinedKeyException, but you can override it in your application.Compatibility notes:
- For backward binary compatibility with
-takeValue:forKey:
‘s behavior, a method whose name matches the pattern-_set<Key>:
is also recognized in step 1. KVC accessor methods whose names start with underscores were deprecated as of Mac OS 10.3 though.- For backward binary compatibility, -unableToSetNilForKey: will be invoked instead of
-setNilValueForKey:
in step 1, if the implementation of-unableToSetNilForKey:
in the receiver’s class is not NSObject’s.- The behavior described in step 2 is different from
-takeValue:forKey:
‘s, in which the instance variable search order is<key>, _<key>
.- For backward binary compatibility with
-takeValue:forKey:
‘s behavior,-handleTakeValue:forUnboundKey:
will be invoked instead of-setValue:forUndefinedKey:
in step 3, if the implementation of-handleTakeValue:forUnboundKey:
in the receiver’s class is not NSObject’s.
翻译如下:
这个方法的默认实现是这样的:
- 搜索该类名称为
-set<Key>:
的存取器方法,如果找到,检查其参数类型。如果参数为nil
,-setNilValueForKey:
方法将会被调用。这个方法的默认实现是抛出NSInvalidArgumentException
异常,不过你可以重写该方法自行实现。如果参数类型为对象类型,该存取器方法会被直接调用,这个参数也会被直接使用。如果参数能被转化为NSNumber/NSValue
类型,参数会在存取器方法被调用之前进行转换。 - 如果存取器方法没有被找到,倘若此时消息接收者的
+accessInstanceVariablesDirectly
属性返回的是YES
,那么按照_<key>, _is<Key>, <key>, is<Key>
的顺序搜索该类的实例变量。如果找到这个实例变量,当其为对象类型时,该实例变量会在旧值释放之后被设置新值。当其为其他类型时,那么按照步骤1中的类型转换规则设置这个实例变量的值。 - 如果存取器方法和实例变量都没有被找到,
-setValue:forUndefinedKey:
方法将会被调用。这个方法的默认实现是抛出NSUndefinedKeyException
异常,不过你可以重写该方法自行实现。
兼容性:
- 为了向后兼容
-takeValue:forKey:
,名称为-_set<Key>:
的方法也会在步骤1中被查找。 - 如果
-unableToSetNilForKey:
的实现不是NSObject
的默认实现(换句话说,自己手动实现了-unableToSetNilForKey:
方法),那在步骤1中,-unableToSetNilForKey:
方法将会代替-setNilValueForKey:
方法被调用。 - 对于
-takeValue:forKey:
,其实例变量的查找顺序不同于步骤2所描述的,调用它时,实例变量查找顺序是<key>, _<key>
。 - 为了向后兼容
-takeValue:forKey:
,如果-handleTakeValue:forUnboundKey:
的实现不是NSObject
的默认实现(换句话说,自己手动实现了-handleTakeValue:forUnboundKey:
方法),那在步骤3中,-handleTakeValue:forUnboundKey:
方法将会代替-setValue:forUndefinedKey:
方法被调用。
流程图如下:
setValue:
方法一
这里使用NSKeyValueCachedSetters
缓存setter
。
1 | // 假定key为@"name" |
方法二
1 | + (NSKeyValueSetter *)_createValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key { |
方法三
1 | + (NSKeyValueSetter *)_createValuePrimitiveSetterWithContainerClassID:(id)containerClassID key:(NSString *)key { |
方法四
1 | + (NSKeyValueSetter *)_createOtherValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key { |
怎么根据Setter设值
1 | void _NSSetUsingKeyValueSetter(id object, NSKeyValueSetter *setter, id value) { |
直接调用Setter中存储的方法实现(getter.implementation)。
setValue:forKeyPath:
1 | - (void)setValue:(id)value forKeyPath:(NSString *)keyPath { |
基本是与取值类似的逻辑。
四、集合对象的KVC
NSArray的KVC
接口
1 | @interface NSArray (NSKeyValueCoding) |
实现
1 | - (id)valueForKey:(NSString *)key { |
求和
1 | // @sum.keyPath, 例如 @"@sum.price", 传递到这个方法中, 参数keyPath为@"price" |
求平均值
1 | // 对 Array中每个对象的keyPath对应值 求平均值 |
求数量
1 | // 获取对象数目 |
求最大值
1 | // 对 Array中每个对象的keyPath对应值 求最大值 |
求最小值
1 | // 对 Array中每个对象的keyPath对应值 求最小值 |
获取数组
1 | // 返回 Array中每个对象的keyPath对应值 组成数组 |
获取去重数组
1 | // 返回 Array中每个对象的keyPath对应值 组成去重数组 |
获取成员数组
1 | // 返回 Array中每个对象的keyPath对应数组的每个成员 组成数组 这里每个keyPath对应值是也是数组,获取的是每个数组展开后组成的总数组 |
获取去重的成员数组
1 | // 返回 Array中每个对象的keyPath对应数组的每个成员 组成的去重复数组. |
获取集合数组
1 | // 返回 Array中每个对象的keyPath对应集合的每个成员 组成的数组. 这里每个keyPath对应值是是集合,获取的是每个集合展开后组成的总数组 |
获取去重的集合数组
1 | // 返回 Array中每个对象的keyPath对应集合的每个成员 组成的去重复数组. |
NSSet的KVC
与NSArray的逻辑基本保持一致。
NSOrderedSet的KVC
与NSArray的逻辑基本保持一致。
NSDictionary的KVC
与NSArray相比,主要区别在于:
1 | - (id)valueForKey:(NSString *)key { |
valueForKey:
取值逻辑多了对@
字符的处理。
valueForKeyPath:
与NSArray的逻辑一致。
五、其他分类的KVC
NSMutableDictionary的KVC
1 | NSMutableDictionary (NSKeyValueCoding) |
相比主类增加的特性是:在NSMutableDictionary
中,如果设置的value
为空,则自动将key
对应的value
移除。
NSUserDefaults的KVC
1 | @implementation NSUserDefaults (NSKeyValueCoding) |
整合了NSDictionary
与NSMutableDictionary
的特色。
- 增加了对
@
字符的处理。 - 如果设置的
value
为空,则自动将key
对应的value
移除。
NSNull的KVC
1 | @implementation NSNull (NSKeyValueCoding) |
对于NSNull
来说,无论怎么设值,取出来的值总是NSNull
对象。
六、总结
纵观全流程,使用KVC与直接使用存取器相比,速度方面稍有逊色,揣测主要原因如下:
- 字符串处理。尤其是含有键路径的时候,使用到递归(函数调用栈)。(当然,含有
@
字符的集合运算符也算。) - 方法查找。流程颇多,尽管有使用
CFSet
作为缓存。 - 装箱拆箱。KVC要求设值参数和取值参数均为对象,这就需要一般值类型和对象类型的相互转换。
这也是为啥现在字典转模型都不使用KVC了,参见《读YYModel》。
KVC并没有那么高性能,那么就无用武之地了吗?非也。
1.访问私有成员变量
对于只给出存取方法的对象,可以使用KVC直接访问私有成员变量。不过可能会破坏封装性,毕竟人家没暴露私有成员变量说明不想让人访问。更多的其实是体现在对系统库上访问上,“一不留神”就用到私有API了,我是乖孩子,不敢这么用,万一被苹果发现整个手百App就要被打回了。。。不过非私有API倒也可以尝试下,FDFullscreenPopGesture就用到了私有成员变量,极其巧妙地解决了全屏侧滑的问题。
2.集合操作
在文章的第四部分【集合对象的KVC】,就已经描述过,求和、求平均值、去重巴拉巴拉,聊胜于无。
3.JSON解析
前些日子,图搜进行框架改版,下发接口需要完全重构。这可是个危险的工作,今年后端已经出现两次问题了,主要是字段的类型出现错误,造成端启动的Crash。端上做了大量的防护工作,防不胜防,而且代码越来越难看。于是我想到了使用KVC解析字段,重新整理现有逻辑,脱敏后大致是这样:1
2
3
4
5
6
7
8
9
10
11
12- (void)handleResponse:(NSDictionary *)response {
if (![response isKindOfClass:NSDictionary.class]) {
return;
}
// 处理苹果业务
[self handleAppleConfigWithResponse:[response valueForKeyPath:@"dataset.config1.apple"]];
// 处理香蕉业务
[self handleBananaConfigWithResponse:[response valueForKeyPath:@"dataset.config2.banana"]];
// 处理橘子业务
[self handleOrangeConfigWithResponse:[response valueForKeyPath:@"dataset.config3.orange"]];
/// ...
}
使用keyPath对应到具体的处理逻辑,不同字段之间逻辑隔离,一个字段出错,并不影响其他字段;字段、方法、逻辑一一对应,后期增加或者删减很方便,新同学熟悉业务逻辑也清晰明了;response是字典,不会出现valueForUndefinedKey
的异常。在每条处理逻辑中做类型保护工作,方便review,不会遗漏。
强烈建议阅读:
https://myzerone.com/posts/2016/10/20/KVC(Key-Value-Coding)/
KVC 和 KVO
源码来自:https://github.com/renjinkui2719/DIS_KVC_KVO 。感谢作者。
参考资料
KVC Collection Operators
iOS KVC
iOS开发之你真的了解了KVC吗?
KVC集合操作符
KVC原理小记
iOS 对象的 setter 方法性能测试