一声旺,概念
KVC(Key-value coding)鍵值編碼尺锚,單看這個(gè)名字可能不太好理解吼蚁。其實(shí)翻譯一下就很簡(jiǎn)單了贮缕,就是指iOS的開(kāi)發(fā)中辙谜,可以允許開(kāi)發(fā)者通過(guò)Key名直接訪問(wèn)對(duì)象的屬性,或者給對(duì)象的屬性賦值感昼。而不需要調(diào)用明確的存取方法装哆。這樣就可以在運(yùn)行時(shí)動(dòng)態(tài)地訪問(wèn)和修改對(duì)象的屬性。而不是在編譯時(shí)確定定嗓,這也是iOS開(kāi)發(fā)中的黑魔法之一蜕琴。很多高級(jí)的iOS開(kāi)發(fā)技巧都是基于KVC實(shí)現(xiàn)的。
二宵溅,KVC在iOS中的定義
無(wú)論是Swift
還是Objective-C
奸绷,KVC的定義都是對(duì)NSObject
的擴(kuò)展來(lái)實(shí)現(xiàn)的(Objective-C中有個(gè)顯式的NSKeyValueCoding
類別名,而Swift
沒(méi)有层玲,也不需要)号醉。所以對(duì)于所有繼承了NSObject
的類型,也就是幾乎所有的Objective-C
對(duì)象都能使用KVC(一些純Swift類和結(jié)構(gòu)體是不支持KVC的)辛块,下面是KVC最為重要的四個(gè)方法
- (nullable id)valueForKey:(NSString *)key; //直接通過(guò)Key來(lái)取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通過(guò)Key來(lái)設(shè)值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通過(guò)KeyPath來(lái)取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通過(guò)KeyPath來(lái)設(shè)值
當(dāng)然因?yàn)镵VC 的定義來(lái)自于基礎(chǔ)框架的Foundation中畔派,所以沒(méi)有開(kāi)源,也就看不到具體代碼的實(shí)現(xiàn)和原來(lái)润绵,所以需要我們借助官方文章來(lái)進(jìn)一步學(xué)習(xí)和了解线椰。
當(dāng)然NSKeyValueCoding類別中還有其他的一些方法,下面列舉一些
+ (BOOL)accessInstanceVariablesDirectly;
//默認(rèn)返回YES尘盼,表示如果沒(méi)有找到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,它可以用來(lái)檢查set的值是否正確躺孝、為不正確的值做一個(gè)替換值或者拒絕設(shè)置新值并返回錯(cuò)誤原因享扔。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API,里面還有一系列這樣的API植袍,如果屬性是一個(gè)NSMutableArray惧眠,那么可以用這個(gè)方法來(lái)返回。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在于个,且沒(méi)有KVC無(wú)法搜索到任何和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í)蘋(píng)果對(duì)一些容器類比如NSArray
或者NSSet
等洞就,KVC有著特殊的實(shí)現(xiàn)。建議有基礎(chǔ)的或者英文好的開(kāi)發(fā)者直接去看蘋(píng)果的官方文檔掀淘,相信你會(huì)對(duì)KVC的理解更上一個(gè)臺(tái)階旬蟋。
三,KVC是怎么尋找Key
KVC是怎么使用的革娄,我相信絕大多數(shù)的開(kāi)發(fā)者都很清楚倾贰,我在這里就不再寫(xiě)簡(jiǎn)單的使用KVC來(lái)設(shè)值和取值的代碼了,首先我們來(lái)探討KVC在內(nèi)部是按什么樣的順序來(lái)尋找key的拦惋。
當(dāng)調(diào)用setValue:
屬性值 forKey:@”name“
的代碼時(shí)匆浙,底層的執(zhí)行機(jī)制如下:
- 1 程序優(yōu)先調(diào)用
set<Key>:
屬性值方法,代碼通過(guò)setter
方法完成設(shè)置厕妖。注意首尼,這里的<key>
是指成員變量名,首字母大小寫(xiě)要符合KVC
的命名規(guī)則,下同 - 2 如果沒(méi)有找到
setName:
方法软能,KVC機(jī)制會(huì)檢查+ (BOOL)accessInstanceVariablesDirectly
方法有沒(méi)有返回YES
迎捺,默認(rèn)該方法會(huì)返回YES
,如果你重寫(xiě)了該方法讓其返回NO
的話查排,那么在這一步KVC會(huì)執(zhí)行setValue:forUndefinedKey:
方法凳枝,不過(guò)一般開(kāi)發(fā)者不會(huì)這么做。所以KVC機(jī)制會(huì)搜索該類里面有沒(méi)有名為_<key>
的成員變量雹嗦,無(wú)論該變量是在類接口處定義,還是在類實(shí)現(xiàn)處定義合是,也無(wú)論用了什么樣的訪問(wèn)修飾符了罪,只在存在以_<key>
命名的變量,KVC
都可以對(duì)該成員變量賦值聪全。 - 3 如果該類即沒(méi)有
set<key>:
方法泊藕,也沒(méi)有_<key>
成員變量,KVC
機(jī)制會(huì)搜索_is<Key>
的成員變量难礼。 - 4 和上面一樣娃圆,如果該類即沒(méi)有
set<Key>:
方法,也沒(méi)有_<key>和_is<Key>
成員變量蛾茉,KVC機(jī)制再會(huì)繼續(xù)搜索<key>
和is<Key>
的成員變量讼呢。再給它們賦值。 - 5 如果上面列出的方法或者成員變量都不存在谦炬,系統(tǒng)將會(huì)執(zhí)行該對(duì)象的
setValue:forUndefinedKey:
方法悦屏,默認(rèn)是拋出異常。
如果開(kāi)發(fā)者想讓這個(gè)類禁用KVC里键思,那么重寫(xiě)+ (BOOL)accessInstanceVariablesDirectly
方法讓其返回NO即可础爬,這樣的話如果KVC沒(méi)有找到set<Key>:屬性名時(shí),會(huì)直接用setValue:forUndefinedKey:方法吼鳞。
3.1 代碼驗(yàn)證賦值過(guò)程
- 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)行賦值過(guò)程赔桌;
LGPerson *person = [[LGPerson alloc] init];
// 1: KVC - 設(shè)置值的過(guò)程 setValue 分析調(diào)用過(guò)程
[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取值&賦值過(guò)程[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取值&賦值過(guò)程[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取值&賦值過(guò)程[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)沒(méi)有任何打印結(jié)果碱茁,所以可以得出結(jié)論是- (void)_setIsName:(NSString *)name
沒(méi)有調(diào)用。
結(jié)論仿贬,KVC的set方法調(diào)用順序?yàn)?setKey ->_setKey -> setIsKey
3.2 實(shí)例變量的賦值過(guò)程
通過(guò)以上的代碼驗(yàn)證纽竣,我們知道了實(shí)例變量的set
方法調(diào)用流程;當(dāng)我們沒(méi)有相應(yīng)的set
方法時(shí),我們?cè)俅慰纯聪鄳?yīng)的示例變量賦值過(guò)程蜓氨。
- 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取值&賦值過(guò)程[9071:191676] LG_Cooci-(null)-(null)-(null)
- 2 注釋掉
_name
方法聋袋,再次打印
NSString *_isName;
NSString *name;
NSString *isName;
// NSString *_name;
// 1: KVC - 設(shè)置值的過(guò)程 setValue 分析調(diào)用過(guò)程
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
打印結(jié)果
2020-10-28 16:57:00.847725+0800 002-KVC取值&賦值過(guò)程[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取值&賦值過(guò)程[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取值&賦值過(guò)程[9312:198897] LG_Cooci
結(jié)論:在沒(méi)有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沒(méi)有找到咪惠,KVC則會(huì)查找
countOf<Key>
,objectIn<Key>AtIndex
或<Key>AtIndexes
格式的方法。如果countOf<Key>
方法和另外兩個(gè)方法中的一個(gè)被找到淋淀,那么就會(huì)返回一個(gè)可以響應(yīng)NSArray
所有方法的代理集合(它是NSKeyValueArray
遥昧,是NSArray
的子類),調(diào)用這個(gè)代理集合的方法朵纷,或者說(shuō)給這個(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 如果上面的方法沒(méi)有找到,那么會(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 如果還沒(méi)有找到,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES(默認(rèn)行為)枪萄,那么和先前的設(shè)值一樣隐岛,會(huì)按_<key>
,_is<Key>
,<key>
,is<Key>
的順序搜索成員變量名,這里不推薦這么做瓷翻,因?yàn)檫@樣直接訪問(wèn)實(shí)例變量破壞了封裝性聚凹,使代碼更脆弱割坠。如果重寫(xiě)了類方法+ (BOOL)accessInstanceVariablesDirectly
返回NO的話,那么會(huì)直接調(diào)用valueForUndefinedKey: - 5 還沒(méi)有找到的話妒牙,調(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è)置值的過(guò)程 setValue 分析調(diào)用過(guò)程
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"取值:%@",[person valueForKey:@"name"]);
打印結(jié)果是
2020-10-28 17:10:39.770668+0800 002-KVC取值&賦值過(guò)程[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取值&賦值過(guò)程[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取值&賦值過(guò)程[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取值&賦值過(guò)程[9654:209106] 取值:_name
結(jié)論,KVC中相關(guān)成員變量的取值方法執(zhí)行順序是 getKey -> Key -> isKey -> _Key
4.2實(shí)例變量的取值過(guò)程
在沒(méi)有實(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取值&賦值過(guò)程[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取值&賦值過(guò)程[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取值&賦值過(guò)程[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取值&賦值過(guò)程[10352:227627] 取值:isName
結(jié)論拴签;成員變量的取值過(guò)程是 _Key -> _isKey -> Key ->isKey
八,總結(jié)
以上就是相關(guān)的KVC的原理旗们,通過(guò)以上的學(xué)習(xí)和總結(jié)蚓哩,了解了KVC 的賦值過(guò)程和取值過(guò)程的順序,執(zhí)行的流程已經(jīng)處理異常的步驟蚪拦,自己對(duì)KVC的理解更進(jìn)一步杖剪。有不足的地方希望各位大神多多指正冻押。