KVC崩溃防护

1,120 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第25天,点击查看活动详情

KVC Crash的主要场景

基于 KVC 基础Getter方法的搜索模式 列出的一大摞过程,我们获取到了两个关键信息:

  1. setValue:forKey:方法在找不到key对应的方法后,会调用setValue: forUndefinedKey:,而这个方法默认会抛出异常。
  2. 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:将值赋值给这个对象。

显而易见了,如果keykeyPath不合法,就会导致抛出异常NSUndefinedKeyException

我们还注意到的一个点是:对于setValue:forKey:valueForKey:都可能涉及到非对象的数据的包装。

对于将非对象包装为对象时不会有什么问题。但是,将对象拆包为非对象时就可能出问题了(手动叹气,真不让人省心)。

我们将一个nil拆包为非对象时,就会调用setNilValueForKey:方法,而这个方法默认会抛出异常NSInvalidArgumentException

还有一个我们平时手抖的时候会出现的问题:key值为nil。这种情况下,setValue:forKey:valueForKey:都会抛出异常NSInvalidArgumentException

来做个总结

KVC使用时造成崩溃的原因有如下几个:

  1. key值为nil,抛出异常NSInvalidArgumentException

  2. value值为nil,并且是为非对象属性设置值,抛出异常NSInvalidArgumentException

  3. 在对象上找不到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