Crash攔截器 - KVC搜索模式及崩潰防護

在本文中,我們將了解到如下內(nèi)容:

  1. KVC 基礎(chǔ)Setter方法的搜索模式
  2. KVC 基礎(chǔ)Getter方法的搜索模式
  3. KVC Crash的主要場景
  4. KVC Crash的防護方案

前言

KVC(Key Value Coding)院水,即鍵值編碼。提供了一個不通過gettersetter简十,而是屬性名來間接訪問對象的屬性的機制檬某。

KVC很好用,這種機制方便我們做很多事螟蝙,比如幫助我們簡化代碼恢恼、解耦代碼以及做一些正常操作做不了的事情(奸笑臉)。

但是由于KVC需要我們硬編碼字符串胰默,在編譯期間無法檢查合法性场斑,所以大家都是一個手抖漓踢,就出現(xiàn)我們最最痛恨的Crash(沮喪臉)。

為了跟KVO引起的Crash永遠說拜拜漏隐,我們將在本篇文章中討論KVO引起Crash的原因喧半,以及做好防護,解決這些Crash青责。

KVC 基礎(chǔ)Setter方法的搜索模式

在執(zhí)行setValue:forKey:方法時挺据,默認會將keyvalue作為參數(shù)輸入,并嘗試在接收調(diào)用的對象中尋找屬性key脖隶,并將value設(shè)置給這個屬性扁耐。

查找屬性key的過程如下:

  1. 按順序查找名稱為set<Key>:_set<Key>:的方法产阱。
    • 如果找到婉称,則使用value(或由value的解包值)設(shè)置給屬性,完成執(zhí)行setValue:forKey:方法心墅。
    • 如果沒有找到酿矢,則執(zhí)行下一步。
  2. 如果類方法accessInstanceVariablesDirectly返回YES怎燥,則按順序查找名稱為_<key>瘫筐、_is<Key><key>is<Key>的實例變量铐姚。
    • 如果找到策肝,則使用value(或由value的解包值)設(shè)置給屬性,完成執(zhí)行setValue:forKey:方法隐绵。
    • 如果未找到之众,或accessInstanceVariablesDirectly返回NO,則執(zhí)行下一步依许。
  3. 調(diào)用setValue: forUndefinedKey:方法棺禾,該方法默認會拋出異常NSUndefinedKeyException

KVC 基礎(chǔ)Getter方法的搜索模式

在執(zhí)行valueForKey:方法時峭跳,默認會將key作為參數(shù)輸入膘婶,并嘗試在接收調(diào)用的對象內(nèi)部執(zhí)行如下過程:

  1. 按順序查找名稱為get<Key><key>蛀醉、is<Key>_<key>的方法悬襟。
    • 如果找到,則調(diào)用該方法并使用結(jié)果執(zhí)行步驟5拯刁。
    • 否則執(zhí)行下一步脊岳。
  2. 查找名稱如countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:的方法割捅。
    • 如果找到了方法countOf<Key>奶躯,并且至少找到了objectIn<Key>AtIndex:<key>AtIndexes:中的一個,系統(tǒng)就會創(chuàng)建一個實現(xiàn)了NSArray所有方法的集合代理對象棺牧,并將該對象返回巫糙。
    • 否則執(zhí)行步驟3。
  3. 查找名稱如countOf<Key>,enumeratorOf<Key>, 和memberOf<Key>:方法颊乘。
    • 如果這三個方法都被找到参淹,系統(tǒng)就會創(chuàng)建一個實現(xiàn)了NSSet所有方法的集合代理對象,并將該對象返回乏悄。
    • 否則執(zhí)行步驟4浙值。
  4. 如果類方法accessInstanceVariablesDirectly返回YES,則按順序查找名稱為_<key>檩小、_is<Key>开呐、<key>is<Key>的實例變量。
    • 如果找到规求,則直接獲取實例變量的值并執(zhí)行步驟5筐付。
    • 如果沒找到,或者accessInstanceVariablesDirectly返回NO阻肿,執(zhí)行步驟6瓦戚。
  5. 分為如下三種情況:
    • 如果返回的屬性值是對象的指針,則直接返回結(jié)果丛塌。
    • 如果返回的屬性值是NSNumber支持的基礎(chǔ)數(shù)據(jù)類型较解,則將其存儲在 NSNumber實例中并返回該值。
    • 如果返回的屬性值是NSNumber不支持的數(shù)據(jù)類型赴邻,則轉(zhuǎn)換為NSValue對象并返回該對象印衔。
  6. 如果上述步驟都失敗了,調(diào)用valueForUndefinedKey:姥敛,該方法默認拋出異常NSUndefinedKeyException奸焙。

KVC Crash的主要場景

基于 KVC 基礎(chǔ)Getter方法的搜索模式 列出的一大摞過程,我們獲取到了兩個關(guān)鍵信息:

  1. setValue:forKey:方法在找不到key對應(yīng)的方法后彤敛,會調(diào)用setValue: forUndefinedKey:忿偷,而這個方法默認會拋出異常。
  2. valueForKey:方法在找不到key對應(yīng)的方法后臊泌,會調(diào)用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:會根據(jù)keyPath一層層的調(diào)用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:會根據(jù)keyPath一層層的調(diào)用valueForKey:找到最終的對象播揪,然后調(diào)用 setValue:forKey:將值賦值給這個對象贮喧。

顯而易見了,如果keykeyPath不合法猪狈,就會導致拋出異常NSUndefinedKeyException箱沦。

我們還注意到的一個點是:對于setValue:forKey:valueForKey:都可能涉及到非對象的數(shù)據(jù)的包裝。

對于將非對象包裝為對象時不會有什么問題雇庙。但是谓形,將對象拆包為非對象時就可能出問題了(手動嘆氣,真不讓人省心)疆前。

我們將一個nil拆包為非對象時寒跳,就會調(diào)用setNilValueForKey:方法,而這個方法默認會拋出異常NSInvalidArgumentException竹椒。

還有一個我們平時手抖的時候會出現(xiàn)的問題:key值為nil童太。這種情況下,setValue:forKey:valueForKey:都會拋出異常NSInvalidArgumentException胸完。

來做個總結(jié)

KVC使用時造成崩潰的原因有如下幾個:

  1. key值為nil书释,拋出異常NSInvalidArgumentException
  2. value值為nil赊窥,并且是為非對象屬性設(shè)置值爆惧,拋出異常NSInvalidArgumentException
  3. 在對象上找不到key對應(yīng)的屬性誓琼,拋出異常NSUndefinedKeyException检激。
    • key不是對象的屬性
    • keyPath不正確

KVC Crash的防護方案

既然已經(jīng)找到了崩潰的原因,現(xiàn)在就是時候來考慮怎么跟這些崩潰說拜拜了腹侣!

  • 對于 key值為nil 這種情況叔收,我們可以利用Method Swizzling方法,在NSObject的分類中傲隶,分別將setValue:forKey:valueForKey:交換為我們自己的方法饺律。并在我們自己的方法中,對key值為nil的情況進行過濾跺株。
  • 對于 value值為nil 這種情況复濒,我們可以通過在NSObject的分類中重寫setNilValueForKey:方法來解決這個問題。
  • 對于 在對象上找不到key對應(yīng)的屬性 這種情況乒省,我們可以通過在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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末砸泛,一起剝皮案震驚了整個濱河市十籍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌唇礁,老刑警劉巖勾栗,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盏筐,居然都是意外死亡围俘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門琢融,熙熙樓的掌柜王于貴愁眉苦臉地迎上來界牡,“玉大人,你說我怎么就攤上這事吏奸』兑荆” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵奋蔚,是天一觀的道長她混。 經(jīng)常有香客問我,道長泊碑,這世上最難降的妖魔是什么坤按? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮馒过,結(jié)果婚禮上臭脓,老公的妹妹穿的比我還像新娘。我一直安慰自己腹忽,他們只是感情好来累,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著窘奏,像睡著了一般嘹锁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上着裹,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天领猾,我揣著相機與錄音,去河邊找鬼骇扇。 笑死摔竿,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的少孝。 我是一名探鬼主播继低,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼稍走!你這毒婦竟也來了郁季?” 一聲冷哼從身側(cè)響起冷溃,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梦裂,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盖淡,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡年柠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了褪迟。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冗恨。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖味赃,靈堂內(nèi)的尸體忽然破棺而出掀抹,到底是詐尸還是另有隱情,我是刑警寧澤心俗,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布傲武,位于F島的核電站,受9級特大地震影響城榛,放射性物質(zhì)發(fā)生泄漏揪利。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一狠持、第九天 我趴在偏房一處隱蔽的房頂上張望疟位。 院中可真熱鬧,春花似錦喘垂、人聲如沸甜刻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽得院。三九已至,卻和暖如春昭齐,著一層夾襖步出監(jiān)牢的瞬間尿招,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工阱驾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留就谜,地道東北人。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓里覆,卻偏偏與公主長得像丧荐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子喧枷,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容