一,概念
KVC(Key-value coding)鍵值編碼,單看這個(gè)名字可能不太好理解。其實(shí)翻譯一下就很簡(jiǎn)單了,就是指iOS的開發(fā)中,可以允許開發(fā)者通過Key名直接訪問對(duì)象的屬性,或者給對(duì)象的屬性賦值。而不需要調(diào)用明確的存取方法。這樣就可以在運(yùn)行時(shí)動(dòng)態(tài)地訪問和修改對(duì)象的屬性。而不是在編譯時(shí)確定,這也是iOS開發(fā)中的黑魔法之一。很多高級(jí)的iOS開發(fā)技巧都是基于KVC實(shí)現(xiàn)的。
二,KVC在iOS中的定義
無論是Swift還是Objective-C,KVC的定義都是對(duì)NSObject的擴(kuò)展來實(shí)現(xiàn)的(Objective-C中有個(gè)顯式的NSKeyValueCoding類別名,而Swift沒有,也不需要)。所以對(duì)于所有繼承了NSObject的類型,也就是幾乎所有的Objective-C對(duì)象都能使用KVC(一些純Swift類和結(jié)構(gòu)體是不支持KVC的),下面是KVC最為重要的四個(gè)方法
- (nullable id)valueForKey:(NSString *)key; //直接通過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通過Key來設(shè)值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通過KeyPath來設(shè)值
當(dāng)然因?yàn)镵VC 的定義來自于基礎(chǔ)框架的Foundation中,所以沒有開源,也就看不到具體代碼的實(shí)現(xiàn)和原來,所以需要我們借助官方文章來進(jìn)一步學(xué)習(xí)和了解。
當(dāng)然NSKeyValueCoding類別中還有其他的一些方法,下面列舉一些
+ (BOOL)accessInstanceVariablesDirectly;
//默認(rèn)返回YES,表示如果沒有找到Set<Key>方法的話,會(huì)按照_key,_iskey,key,iskey的順序搜索成員,設(shè)置成NO就不這樣搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值正確性驗(yàn)證的API,它可以用來檢查set的值是否正確、為不正確的值做一個(gè)替換值或者拒絕設(shè)置新值并返回錯(cuò)誤原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個(gè)NSMutableArray,那么可以用這個(gè)方法來返回。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且沒有KVC無法搜索到任何和Key有關(guān)的字段或者屬性,則會(huì)調(diào)用這個(gè)方法,默認(rèn)是拋出異常。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個(gè)方法一樣,但這個(gè)方法是設(shè)值。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法時(shí)面給Value傳nil,則會(huì)調(diào)用這個(gè)方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對(duì)應(yīng)的Value,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典。
上面的這些方法在碰到特殊情況或者有特殊需求還是會(huì)用到的,所以也是可以了解一下。后面的代碼示例會(huì)有講到其中的一些方法。
同時(shí)蘋果對(duì)一些容器類比如NSArray或者NSSet等,KVC有著特殊的實(shí)現(xiàn)。建議有基礎(chǔ)的或者英文好的開發(fā)者直接去看蘋果的官方文檔,相信你會(huì)對(duì)KVC的理解更上一個(gè)臺(tái)階。
三,KVC是怎么尋找Key
KVC是怎么使用的,我相信絕大多數(shù)的開發(fā)者都很清楚,我在這里就不再寫簡(jiǎn)單的使用KVC來設(shè)值和取值的代碼了,首先我們來探討KVC在內(nèi)部是按什么樣的順序來尋找key的。

當(dāng)調(diào)用setValue:屬性值 forKey:@”name“的代碼時(shí),底層的執(zhí)行機(jī)制如下:
- 1 程序優(yōu)先調(diào)用
set<Key>:屬性值方法,代碼通過setter方法完成設(shè)置。注意,這里的<key>是指成員變量名,首字母大小寫要符合KVC的命名規(guī)則,下同 - 2 如果沒有找到
setName:方法,KVC機(jī)制會(huì)檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認(rèn)該方法會(huì)返回YES,如果你重寫了該方法讓其返回NO的話,那么在這一步KVC會(huì)執(zhí)行setValue:forUndefinedKey:方法,不過一般開發(fā)者不會(huì)這么做。所以KVC機(jī)制會(huì)搜索該類里面有沒有名為_<key>的成員變量,無論該變量是在類接口處定義,還是在類實(shí)現(xiàn)處定義,也無論用了什么樣的訪問修飾符,只在存在以_<key>命名的變量,KVC都可以對(duì)該成員變量賦值。 - 3 如果該類即沒有
set<key>:方法,也沒有_<key>成員變量,KVC機(jī)制會(huì)搜索_is<Key>的成員變量。 - 4 和上面一樣,如果該類即沒有
set<Key>:方法,也沒有_<key>和_is<Key>成員變量,KVC機(jī)制再會(huì)繼續(xù)搜索<key>和is<Key>的成員變量。再給它們賦值。 - 5 如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會(huì)執(zhí)行該對(duì)象的
setValue:forUndefinedKey:方法,默認(rèn)是拋出異常。
如果開發(fā)者想讓這個(gè)類禁用KVC里,那么重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話如果KVC沒有找到set<Key>:屬性名時(shí),會(huì)直接用setValue:forUndefinedKey:方法。
3.1 代碼驗(yàn)證賦值過程
- 1 我們對(duì)一個(gè)類聲明了四個(gè)屬性
name,_isName,isName和_name,
@interface LGPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
- 2 在控制器中我們先實(shí)例化一個(gè)
person對(duì)象,從而對(duì)調(diào)用KVC 進(jìn)行賦值過程;
LGPerson *person = [[LGPerson alloc] init];
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"LG_Cooci" forKey:@"name"]
- 3 在person類中按先后順序執(zhí)行setter方法,看看執(zhí)行的流程
第一步執(zhí)行setName方法
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
打印結(jié)果
2020-10-28 16:36:11.503798+0800 002-KVC取值&賦值過程[8696:180841] -[LGPerson setName:] - LG_Cooci
第二步,注釋掉setName方法再次打印結(jié)果
//- (void)setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
//}
//
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
打印結(jié)果
2020-10-28 16:41:37.143073+0800 002-KVC取值&賦值過程[8825:184607] -[LGPerson _setName:] - LG_Cooci
第三步注釋掉_setName再次打印
//- (void)setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
//}
//
//- (void)_setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
//}
- (void)setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
打印結(jié)果
2020-10-28 16:43:45.832732+0800 002-KVC取值&賦值過程[8883:186519] -[LGPerson setIsName:] - LG_Cooci
第四步,注釋掉setIsName 再次打印結(jié)果
//- (void)setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
//}
//
//- (void)_setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
//}
//- (void)setIsName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
//}
- (void)_setIsName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
可以看到控制臺(tái)沒有任何打印結(jié)果,所以可以得出結(jié)論是- (void)_setIsName:(NSString *)name沒有調(diào)用。
結(jié)論,KVC的set方法調(diào)用順序?yàn)?setKey ->_setKey -> setIsKey
3.2 實(shí)例變量的賦值過程
通過以上的代碼驗(yàn)證,我們知道了實(shí)例變量的set方法調(diào)用流程;當(dāng)我們沒有相應(yīng)的set方法時(shí),我們?cè)俅慰纯聪鄳?yīng)的示例變量賦值過程。
- 1 我們所有的成員變量進(jìn)行監(jiān)控
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
打印結(jié)果
2020-10-28 16:51:28.428396+0800 002-KVC取值&賦值過程[9071:191676] LG_Cooci-(null)-(null)-(null)
- 2 注釋掉
_name方法,再次打印
NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
打印結(jié)果
2020-10-28 16:57:00.847725+0800 002-KVC取值&賦值過程[9199:195725] LG_Cooci-(null)-(null)
- 3 再次注釋掉
_isName再打印結(jié)果
// NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@-%@",person->name,person->isName);
打印結(jié)果
2020-10-28 16:58:39.576608+0800 002-KVC取值&賦值過程[9254:197268] LG_Cooci-(null)
- 4 再次注釋掉
name再次打印結(jié)果
// NSString *_isName;
// NSString *name;
NSString *isName;
// NSString *_name;
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@",person->isName);
打印結(jié)果
2020-10-28 17:00:24.788572+0800 002-KVC取值&賦值過程[9312:198897] LG_Cooci
結(jié)論:在沒有set方法的時(shí)候,KVC 對(duì)成員變量的賦值順序是 _key -> _isKey -> key ->isKey
四,valueForkey 取值的內(nèi)部原理
當(dāng)調(diào)用valueForKey:@”name“的代碼時(shí),KVC對(duì)key的搜索方式不同于setValue:屬性值 forKey:@”name“,其搜索方式如下:

- 1 首先按
get<Key>,<key>,is<Key>的順序方法查找getter方法,找到的話會(huì)直接調(diào)用。如果是BOOL或者Int等值類型, 會(huì)將其包裝成一個(gè)NSNumber對(duì)象。 - 2 如果上面的getter沒有找到,KVC則會(huì)查找
countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外兩個(gè)方法中的一個(gè)被找到,那么就會(huì)返回一個(gè)可以響應(yīng)NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調(diào)用這個(gè)代理集合的方法,或者說給這個(gè)代理集合發(fā)送屬于NSArray的方法,就會(huì)以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes這幾個(gè)方法組合的形式調(diào)用。還有一個(gè)可選的get<Key>:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標(biāo)準(zhǔn)命名方法,包括方法簽名。 - 3 如果上面的方法沒有找到,那么會(huì)同時(shí)查找countOf<Key>,
enumeratorOf<Key>,memberOf<Key>格式的方法。如果這三個(gè)方法都找到,那么就返回一個(gè)可以響應(yīng)NSSet所的方法的代理集合,和上面一樣,給這個(gè)代理集合發(fā)NSSet的消息,就會(huì)以countOf<Key>,enumeratorOf<Key>,memberOf<Key>組合的形式調(diào)用。 - 4 如果還沒有找到,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認(rèn)行為),那么和先前的設(shè)值一樣,會(huì)按_<key>,_is<Key>,<key>,is<Key>的順序搜索成員變量名,這里不推薦這么做,因?yàn)檫@樣直接訪問實(shí)例變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那么會(huì)直接調(diào)用valueForUndefinedKey: - 5 還沒有找到的話,調(diào)用
valueForUndefinedKey:
4.1代碼驗(yàn)證KVC 的get方法順序
在Person類中分別實(shí)現(xiàn)getName、name,isName和_name方法
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
- 1 在控制器中對(duì)相關(guān)的屬性進(jìn)行取值內(nèi)容;
LGPerson *person = [[LGPerson alloc] init];
// 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"取值:%@",[person valueForKey:@"name"]);
打印結(jié)果是
2020-10-28 17:10:39.770668+0800 002-KVC取值&賦值過程[9557:205906] 取值:getName
- 2 注釋掉
getName方法,再次打印
//- (NSString *)getName{
// return NSStringFromSelector(_cmd);
//}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
打印結(jié)果
2020-10-28 17:11:43.928335+0800 002-KVC取值&賦值過程[9593:207045] 取值:name
- 3 再次注釋掉
name方法,打印
//- (NSString *)getName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)name{
// return NSStringFromSelector(_cmd);
//}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
打印結(jié)果
2020-10-28 17:12:33.678782+0800 002-KVC取值&賦值過程[9622:208078] 取值:isName
- 4 再注釋
isName方法打印
//- (NSString *)getName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)name{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)isName{
// return NSStringFromSelector(_cmd);
//}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
打印結(jié)果
2020-10-28 17:13:29.096733+0800 002-KVC取值&賦值過程[9654:209106] 取值:_name
結(jié)論,KVC中相關(guān)成員變量的取值方法執(zhí)行順序是 getKey -> Key -> isKey -> _Key
4.2實(shí)例變量的取值過程
在沒有實(shí)現(xiàn)任何get方法的情況下,
//- (NSString *)getName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)name{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)isName{
// return NSStringFromSelector(_cmd);
//}
//- (NSString *)_name{
// return NSStringFromSelector(_cmd);
//}
- 1 我們?cè)诳刂破髦袑?duì)所有的成員變量進(jìn)行一個(gè)賦值操作
person->_name = @"_name";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
打印結(jié)果是
2020-10-28 17:25:02.021100+0800 002-KVC取值&賦值過程[9964:217084] 取值:_name
- 2 注釋掉
_name再次打印結(jié)果
NSString *_isName;
NSString *name;
NSString *isName;
//NSString *_name;
person->_isName = @"_isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
打印結(jié)果
2020-10-28 17:29:25.962006+0800 002-KVC取值&賦值過程[10103:221196] 取值:_isName
- 3 再次注釋掉
_isName再次打印
// NSString *_isName;
NSString *name;
NSString *isName;
//NSString *_name;
person->name = @"name";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
打印結(jié)果
2020-10-28 17:35:22.496774+0800 002-KVC取值&賦值過程[10281:225879] 取值:name
- 4 再次注釋掉
name在打印結(jié)果
// NSString *_isName;
// NSString *name;
NSString *isName;
//NSString *_name;
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
打印結(jié)果是
2020-10-28 17:37:17.713126+0800 002-KVC取值&賦值過程[10352:227627] 取值:isName
結(jié)論;成員變量的取值過程是 _Key -> _isKey -> Key ->isKey
八,總結(jié)
以上就是相關(guān)的KVC的原理,通過以上的學(xué)習(xí)和總結(jié),了解了KVC 的賦值過程和取值過程的順序,執(zhí)行的流程已經(jīng)處理異常的步驟,自己對(duì)KVC的理解更進(jìn)一步。有不足的地方希望各位大神多多指正。