在本文中,我们将了解到如下内容:
- KVC 基础
Setter
方法的搜索模式- KVC 基础
Getter
方法的搜索模式- KVC Crash的主要场景
- KVC Crash的防护方案
前言
KVC(Key Value Coding),即键值编码。提供了一个不通过getter
和setter
,而是属性名
或键
来间接访问对象的属性的机制。
KVC很好用,这种机制方便我们做很多事,比如帮助我们简化代码、解耦代码以及做一些正常操作做不了的事情(奸笑脸)。
但是由于KVC需要我们硬编码字符串,在编译期间无法检查合法性,所以大家都是一个手抖,就出现我们最最痛恨的Crash
(沮丧脸)。
为了跟KVO引起的Crash
永远说拜拜,我们将在本篇文章中讨论KVO引起Crash
的原因,以及做好防护,解决这些Crash
。
KVC 基础Setter
方法的搜索模式
在执行setValue:forKey:
方法时,默认会将key
和value
作为参数输入,并尝试在接收调用的对象中寻找属性key
,并将value
设置给这个属性。
查找属性key
的过程如下:
- 按顺序查找名称为
set<Key>:
、_set<Key>:
的方法。- 如果找到,则使用
value
(或由value
的解包值)设置给属性,完成执行setValue:forKey:
方法。 - 如果没有找到,则执行下一步。
- 如果找到,则使用
- 如果类方法
accessInstanceVariablesDirectly
返回YES
,则按顺序查找名称为_<key>
、_is<Key>
、<key>
或is<Key>
的实例变量。- 如果找到,则使用
value
(或由value
的解包值)设置给属性,完成执行setValue:forKey:
方法。 - 如果未找到,或
accessInstanceVariablesDirectly
返回NO
,则执行下一步。
- 如果找到,则使用
- 调用
setValue: forUndefinedKey:
方法,该方法默认会抛出异常NSUndefinedKeyException
。
KVC 基础Getter
方法的搜索模式
在执行valueForKey:
方法时,默认会将key
作为参数输入,并尝试在接收调用的对象内部执行如下过程:
- 按顺序查找名称为
get<Key>
、<key>
、is<Key>
或_<key>
的方法。
- 如果找到,则调用该方法并使用结果执行步骤5。
- 否则执行下一步。
- 如果找到,则调用该方法并使用结果执行步骤5。
- 查找名称如
countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
的方法。
- 如果找到了方法
countOf<Key>
,并且至少找到了objectIn<Key>AtIndex:
和<key>AtIndexes:
中的一个,系统就会创建一个实现了NSArray
所有方法的集合代理对象,并将该对象返回。
- 否则执行步骤3。
- 如果找到了方法
- 查找名称如
countOf<Key>
,enumeratorOf<Key>
, 和memberOf<Key>:
方法。
- 如果这三个方法都被找到,系统就会创建一个实现了
NSSet
所有方法的集合代理对象,并将该对象返回。
- 否则执行步骤4。
- 如果这三个方法都被找到,系统就会创建一个实现了
- 如果类方法
accessInstanceVariablesDirectly
返回YES,则按顺序查找名称为_<key>
、_is<Key>
、<key>
或is<Key>
的实例变量。
- 如果找到,则直接获取实例变量的值并执行步骤5。
- 如果没找到,或者
accessInstanceVariablesDirectly
返回NO
,执行步骤6。
- 如果找到,则直接获取实例变量的值并执行步骤5。
- 分为如下三种情况:
- 如果返回的属性值是对象的指针,则直接返回结果。
- 如果返回的属性值是
NSNumber
支持的基础数据类型,则将其存储在NSNumber
实例中并返回该值。 - 如果返回的属性值是
NSNumber
不支持的数据类型,则转换为NSValue
对象并返回该对象。
- 如果返回的属性值是对象的指针,则直接返回结果。
- 如果上述步骤都失败了,调用
valueForUndefinedKey:
,该方法默认抛出异常NSUndefinedKeyException
。
KVC Crash的主要场景
基于 KVC 基础Getter
方法的搜索模式 列出的一大摞过程,我们获取到了两个关键信息:
-
setValue:forKey:
方法在找不到key
对应的方法后,会调用setValue: forUndefinedKey:
,而这个方法默认会抛出异常。 -
valueForKey:
方法在找不到key
对应的方法后,会调用valueForUndefinedKey:
,这个方法也会抛出异常。
我们看看valueForKeyPath:
的介绍:
The default implementation gets the destination object for each relationship using valueForKey: and returns the result of a valueForKey: message to the final object.
大致是说:valueForKeyPath:
会根据keyPath
一层层的调用valueForKey:
,从而获取最终的值。
我们再看看setValue:forKeyPath:
的介绍:
The default implementation of this method gets the destination object for each relationship using valueForKey:, and sends the final object a setValue:forKey: message.
和我们预期的一样,setValue:forKeyPath:
会根据keyPath
一层层的调用valueForKey:
找到最终的对象,然后调用 setValue:forKey:
将值赋值给这个对象。
显而易见了,如果key
或keyPath
不合法,就会导致抛出异常NSUndefinedKeyException
。
我们还注意到的一个点是:对于setValue:forKey:
和valueForKey:
都可能涉及到非对象的数据的包装。
对于将非对象包装为对象时不会有什么问题。但是,将对象拆包为非对象时就可能出问题了(手动叹气,真不让人省心)。
我们将一个nil
拆包为非对象时,就会调用setNilValueForKey:
方法,而这个方法默认会抛出异常NSInvalidArgumentException
。
还有一个我们平时手抖的时候会出现的问题:key
值为nil
。这种情况下,setValue:forKey:
和valueForKey:
都会抛出异常NSInvalidArgumentException
。
来做个总结
KVC使用时造成崩溃的原因有如下几个:
-
key
值为nil
,抛出异常NSInvalidArgumentException
。 -
value
值为nil
,并且是为非对象属性设置值,抛出异常NSInvalidArgumentException
。 - 在对象上找不到
key
对应的属性,抛出异常NSUndefinedKeyException
。-
key
不是对象的属性 -
keyPath
不正确
-
KVC Crash的防护方案
既然已经找到了崩溃的原因,现在就是时候来考虑怎么跟这些崩溃说拜拜了!
- 对于
key
值为nil
这种情况,我们可以利用Method Swizzling
方法,在NSObject
的分类中,分别将setValue:forKey:
和valueForKey:
交换为我们自己的方法。并在我们自己的方法中,对key
值为nil
的情况进行过滤。 - 对于
value
值为nil
这种情况,我们可以通过在NSObject
的分类中重写setNilValueForKey:
方法来解决这个问题。 - 对于 在对象上找不到
key
对应的属性 这种情况,我们可以通过在NSObject
的分类中重写valueForUndefinedKey:
和setValue: forUndefinedKey:
方法来解决这个问题。
至此,上文中提到的KVO的3种崩溃情况就都解决了(手动开心)。下面贴出具体代码:
@implementation NSObject (KVCCrashPreventor)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self kvc_exchangeSelector:@selector(setValue:forKey:) toSelector:@selector(kvc_setValue:forKey:)];
[self kvc_exchangeSelector:@selector(valueForKey:) toSelector:@selector(kvc_valueForKey:)];
});
}
+ (void)kvc_exchangeSelector:(SEL)selector toSelector:(SEL)toSelector {
Method method = class_getInstanceMethod(self.class, selector);
Method toMethod = class_getInstanceMethod(self.class, toSelector);
method_exchangeImplementations(method, toMethod);
}
- (void)kvc_setValue:(id)value forKey:(NSString *)key {
if (key.length <= 0) {
NSLog(@"[%@ setValue:forKey:]: attempt to set a value for a nil key", self.class);
return;
}
[self kvc_setValue:value forKey:key];
}
- (id)kvc_valueForKey:(NSString *)key {
if (key == nil) {
NSLog(@"[%@ valueForKey:]: attempt to retrieve a value for a nil key", self.class);
return nil;
}
return [self kvc_valueForKey:key];
}
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"[<%@ %p> setNilValueForKey]: could not set nil as the value for the key length.", self.class, self);
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"[<%@ %p> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key %@.", self.class, self, key);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"[<%@ %p> valueForUndefinedKey:]: this class is not key value coding-compliant for the key: %@", self.class, self, key);
return nil;
}
@end