在本文中,我們將了解到如下內(nèi)容:
- KVC 基礎(chǔ)
Setter
方法的搜索模式- KVC 基礎(chǔ)
Getter
方法的搜索模式- KVC Crash的主要場景
- KVC Crash的防護方案
前言
KVC(Key Value Coding)院水,即鍵值編碼。提供了一個不通過getter
和setter
简十,而是屬性名
或鍵
來間接訪問對象的屬性的機制檬某。
KVC很好用,這種機制方便我們做很多事螟蝙,比如幫助我們簡化代碼恢恼、解耦代碼以及做一些正常操作做不了的事情(奸笑臉)。
但是由于KVC需要我們硬編碼字符串胰默,在編譯期間無法檢查合法性场斑,所以大家都是一個手抖漓踢,就出現(xiàn)我們最最痛恨的Crash
(沮喪臉)。
為了跟KVO引起的Crash
永遠說拜拜漏隐,我們將在本篇文章中討論KVO引起Crash
的原因喧半,以及做好防護,解決這些Crash
青责。
KVC 基礎(chǔ)Setter
方法的搜索模式
在執(zhí)行setValue:forKey:
方法時挺据,默認會將key
和value
作為參數(shù)輸入,并嘗試在接收調(diào)用的對象中尋找屬性key
脖隶,并將value
設(shè)置給這個屬性扁耐。
查找屬性key
的過程如下:
- 按順序查找名稱為
set<Key>:
、_set<Key>:
的方法产阱。- 如果找到婉称,則使用
value
(或由value
的解包值)設(shè)置給屬性,完成執(zhí)行setValue:forKey:
方法心墅。 - 如果沒有找到酿矢,則執(zhí)行下一步。
- 如果找到婉称,則使用
- 如果類方法
accessInstanceVariablesDirectly
返回YES
怎燥,則按順序查找名稱為_<key>
瘫筐、_is<Key>
、<key>
或is<Key>
的實例變量铐姚。- 如果找到策肝,則使用
value
(或由value
的解包值)設(shè)置給屬性,完成執(zhí)行setValue:forKey:
方法隐绵。 - 如果未找到之众,或
accessInstanceVariablesDirectly
返回NO
,則執(zhí)行下一步依许。
- 如果找到策肝,則使用
- 調(diào)用
setValue: forUndefinedKey:
方法棺禾,該方法默認會拋出異常NSUndefinedKeyException
。
KVC 基礎(chǔ)Getter
方法的搜索模式
在執(zhí)行valueForKey:
方法時峭跳,默認會將key
作為參數(shù)輸入膘婶,并嘗試在接收調(diào)用的對象內(nèi)部執(zhí)行如下過程:
- 按順序查找名稱為
get<Key>
、<key>
蛀醉、is<Key>
或_<key>
的方法悬襟。
- 如果找到,則調(diào)用該方法并使用結(jié)果執(zhí)行步驟5拯刁。
- 否則執(zhí)行下一步脊岳。
- 如果找到,則調(diào)用該方法并使用結(jié)果執(zhí)行步驟5拯刁。
- 查找名稱如
countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
的方法割捅。
- 如果找到了方法
countOf<Key>
奶躯,并且至少找到了objectIn<Key>AtIndex:
和<key>AtIndexes:
中的一個,系統(tǒng)就會創(chuàng)建一個實現(xiàn)了NSArray
所有方法的集合代理對象棺牧,并將該對象返回巫糙。
- 否則執(zhí)行步驟3。
- 如果找到了方法
- 查找名稱如
countOf<Key>
,enumeratorOf<Key>
, 和memberOf<Key>:
方法颊乘。
- 如果這三個方法都被找到参淹,系統(tǒng)就會創(chuàng)建一個實現(xiàn)了
NSSet
所有方法的集合代理對象,并將該對象返回乏悄。
- 否則執(zhí)行步驟4浙值。
- 如果這三個方法都被找到参淹,系統(tǒng)就會創(chuàng)建一個實現(xiàn)了
- 如果類方法
accessInstanceVariablesDirectly
返回YES,則按順序查找名稱為_<key>
檩小、_is<Key>
开呐、<key>
或is<Key>
的實例變量。
- 如果找到规求,則直接獲取實例變量的值并執(zhí)行步驟5筐付。
- 如果沒找到,或者
accessInstanceVariablesDirectly
返回NO
阻肿,執(zhí)行步驟6瓦戚。
- 如果找到规求,則直接獲取實例變量的值并執(zhí)行步驟5筐付。
- 分為如下三種情況:
- 如果返回的屬性值是對象的指針,則直接返回結(jié)果丛塌。
- 如果返回的屬性值是
NSNumber
支持的基礎(chǔ)數(shù)據(jù)類型较解,則將其存儲在NSNumber
實例中并返回該值。 - 如果返回的屬性值是
NSNumber
不支持的數(shù)據(jù)類型赴邻,則轉(zhuǎn)換為NSValue
對象并返回該對象印衔。
- 如果返回的屬性值是對象的指針,則直接返回結(jié)果丛塌。
- 如果上述步驟都失敗了,調(diào)用
valueForUndefinedKey:
姥敛,該方法默認拋出異常NSUndefinedKeyException
奸焙。
KVC Crash的主要場景
基于 KVC 基礎(chǔ)Getter
方法的搜索模式 列出的一大摞過程,我們獲取到了兩個關(guān)鍵信息:
-
setValue:forKey:
方法在找不到key
對應(yīng)的方法后彤敛,會調(diào)用setValue: forUndefinedKey:
忿偷,而這個方法默認會拋出異常。 -
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:
將值賦值給這個對象贮喧。
顯而易見了,如果key
或keyPath
不合法猪狈,就會導致拋出異常NSUndefinedKeyException
箱沦。
我們還注意到的一個點是:對于setValue:forKey:
和valueForKey:
都可能涉及到非對象的數(shù)據(jù)的包裝。
對于將非對象包裝為對象時不會有什么問題雇庙。但是谓形,將對象拆包為非對象時就可能出問題了(手動嘆氣,真不讓人省心)疆前。
我們將一個nil
拆包為非對象時寒跳,就會調(diào)用setNilValueForKey:
方法,而這個方法默認會拋出異常NSInvalidArgumentException
竹椒。
還有一個我們平時手抖的時候會出現(xiàn)的問題:key
值為nil
童太。這種情況下,setValue:forKey:
和valueForKey:
都會拋出異常NSInvalidArgumentException
胸完。
來做個總結(jié)
KVC使用時造成崩潰的原因有如下幾個:
-
key
值為nil
书释,拋出異常NSInvalidArgumentException
。 -
value
值為nil
赊窥,并且是為非對象屬性設(shè)置值爆惧,拋出異常NSInvalidArgumentException
。 - 在對象上找不到
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