Crash拦截器 - KVC搜索模式及崩溃防护

在本文中,我们将了解到如下内容:

  1. KVC 基础Setter方法的搜索模式
  2. KVC 基础Getter方法的搜索模式
  3. KVC Crash的主要场景
  4. KVC Crash的防护方案

前言

KVC(Key Value Coding),即键值编码。提供了一个不通过gettersetter,而是属性名来间接访问对象的属性的机制。

KVC很好用,这种机制方便我们做很多事,比如帮助我们简化代码、解耦代码以及做一些正常操作做不了的事情(奸笑脸)。

但是由于KVC需要我们硬编码字符串,在编译期间无法检查合法性,所以大家都是一个手抖,就出现我们最最痛恨的Crash(沮丧脸)。

为了跟KVO引起的Crash永远说拜拜,我们将在本篇文章中讨论KVO引起Crash的原因,以及做好防护,解决这些Crash

KVC 基础Setter方法的搜索模式

在执行setValue:forKey:方法时,默认会将keyvalue作为参数输入,并尝试在接收调用的对象中寻找属性key,并将value设置给这个属性。

查找属性key的过程如下:

  1. 按顺序查找名称为set<Key>:_set<Key>:的方法。
    • 如果找到,则使用value(或由value的解包值)设置给属性,完成执行setValue:forKey:方法。
    • 如果没有找到,则执行下一步。
  2. 如果类方法accessInstanceVariablesDirectly返回YES,则按顺序查找名称为_<key>_is<Key><key>is<Key>的实例变量。
    • 如果找到,则使用value(或由value的解包值)设置给属性,完成执行setValue:forKey:方法。
    • 如果未找到,或accessInstanceVariablesDirectly返回NO,则执行下一步。
  3. 调用setValue: forUndefinedKey:方法,该方法默认会抛出异常NSUndefinedKeyException

KVC 基础Getter方法的搜索模式

在执行valueForKey:方法时,默认会将key作为参数输入,并尝试在接收调用的对象内部执行如下过程:

  1. 按顺序查找名称为get<Key><key>is<Key>_<key>的方法。
    • 如果找到,则调用该方法并使用结果执行步骤5。
    • 否则执行下一步。
  2. 查找名称如countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:的方法。
    • 如果找到了方法countOf<Key>,并且至少找到了objectIn<Key>AtIndex:<key>AtIndexes:中的一个,系统就会创建一个实现了NSArray所有方法的集合代理对象,并将该对象返回。
    • 否则执行步骤3。
  3. 查找名称如countOf<Key>,enumeratorOf<Key>, 和memberOf<Key>:方法。
    • 如果这三个方法都被找到,系统就会创建一个实现了NSSet所有方法的集合代理对象,并将该对象返回。
    • 否则执行步骤4。
  4. 如果类方法accessInstanceVariablesDirectly返回YES,则按顺序查找名称为_<key>_is<Key><key>is<Key>的实例变量。
    • 如果找到,则直接获取实例变量的值并执行步骤5。
    • 如果没找到,或者accessInstanceVariablesDirectly返回NO,执行步骤6。
  5. 分为如下三种情况:
    • 如果返回的属性值是对象的指针,则直接返回结果。
    • 如果返回的属性值是NSNumber支持的基础数据类型,则将其存储在 NSNumber实例中并返回该值。
    • 如果返回的属性值是NSNumber不支持的数据类型,则转换为NSValue对象并返回该对象。
  6. 如果上述步骤都失败了,调用valueForUndefinedKey:,该方法默认抛出异常NSUndefinedKeyException

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容