KVC
全稱Key-Value Coding
,俗稱鍵值編碼
掸掏,是由NSKeyValueCoding
非正式協(xié)議啟用的一種機制茁影,對象采用該協(xié)議可以間接訪問其屬性
,可以通過一個字符串Key
來訪問某個屬性
丧凤。這種間接訪問機制補充了是咧變量及其相關(guān)的訪問器方法所提供的直接訪問募闲。
相關(guān)API
- 通過Key取值和設值
//直接通過Key來取值
- (nullable id)valueForKey:(NSString *)key;
//通過Key來設值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- 通過
keyPath(路由)
取值和設值
//通過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通過KeyPath來設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- 其他方法
//默認返回YES,表示如果沒有找到Set<Key>方法的話息裸,會按照_key蝇更,_iskey,key呼盆,iskey的順序搜索成員年扩,設置成NO就不這樣搜索
+ (BOOL)accessInstanceVariablesDirectly;
//KVC提供屬性值正確性驗證的API,它可以用來檢查set的值是否正確访圃、為不正確的值做一個替換值或者拒絕設置新值并返回錯誤原因厨幻。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//這是集合操作的API,里面還有一系列這樣的API腿时,如果屬性是一個NSMutableArray况脆,那么可以用這個方法來返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//如果Key不存在批糟,且KVC無法搜索到任何和Key有關(guān)的字段或者屬性格了,則會調(diào)用這個方法,默認是拋出異常徽鼎。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//和上一個方法一樣盛末,但這個方法是設值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果你在SetValue方法時面給Value傳nil否淤,則會調(diào)用這個方法
- (void)setNilValueForKey:(NSString *)key;
//輸入一組key,返回該組key對應的Value悄但,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典石抡。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
KVC底層原理
通過setValue:forKey
方法聲明檐嚣,我們發(fā)現(xiàn)是在Foundation
框架中,由于Foundation
是不開源的啰扛,我們可以通過下面幾種方式來探索
- 通過
Hopper
反匯編嚎京,查看偽代碼 -
Github
搜索相關(guān)文檔 - 通過蘋果官方文檔
下面我們通過官方文檔來查看KVC的底層原理
KVC設值流程
-
【第一步】查找三種
setter
方法,查找順序是set<Key> --> _set<Key> --> setIs<Key>
- 如果
包含其中任意一個
侠讯,直接設值屬性value(key是指成員變量名
挖藏,首字符大小寫需要符合KVC的命名規(guī)范
) - 如果沒有,進入【第二步】
- 如果
-
【第二步】判斷
accessInstanceVariablesDirectly
是否返回YES
- 如果返回YES厢漩,則查找實例變量進行賦值膜眠,查找順序是
_<Key> --> _is<Key> --> <Key> -- > is<Key>
- 如果找到任意一個就進行賦值
- 如果沒有找到進入【第三步】
- 如果返回YES厢漩,則查找實例變量進行賦值膜眠,查找順序是
-
【第三步】如果
setter方法
和實例變量
都沒找到,會執(zhí)行setValue:forUndefinedKey:
,默認拋出NSUndefinedKeyException
異常
KVC取值流程
-
【第一步】查找
getter方法
宵膨,查找順序是get<Key> --> <Key> --> is<Key> --> _<Key>
- 找到架谎,執(zhí)行【第五步】
- 沒有找到,執(zhí)行【第二步】
-
【第二步】在實例中搜索
countOf<Key>
辟躏、objectIn<Key>AtIndex:
谷扣、objectsAtIndexes :
如果找到
countOf<Key>
和其余任意一個,創(chuàng)建一個響應所有NSArray方法
的集合代理
對象并返回捎琐,然后代理對象將收到的所有NSArray消息轉(zhuǎn)化成countOf<Key>会涎,objectIn<Key>AtIndex:和<key>AtIndexes:
消息的組合,用來創(chuàng)建鍵值編碼對象瑞凑,如果原始對象還實現(xiàn)了get<Key>:range:
之類的可選方法末秃,則代理對象也將在適當?shù)臅r候使用該方法,實際上籽御,代理對象與與鍵值編碼兼容的對象一起使用练慕,可以使基礎屬性的行為就好像它是NSArray,即使不是技掏。(注意:方法名的命名規(guī)則要符合KVC的標準命名方法铃将,包括方法簽名。)沒找到哑梳,執(zhí)行【第三步】
-
【第三步】同時查找
countOf <Key>劲阎,enumeratorOf<Key>和memberOf<Key>
這三個方法- 如果三個方法都找到,創(chuàng)建一個響應所有
NSSet
方法的集合代理對象并將其返回 - 沒有找到鸠真,執(zhí)行【第四步】
- 如果三個方法都找到,創(chuàng)建一個響應所有
-
【第四步】檢測類方法
InstanceVariablesDirectly
是否為YES
哪工,依次搜索_<key>,_is<Key>弧哎,<key>或is<Key>
的實例變量- 如果找到,
直接獲取實例變量的值
稚虎,執(zhí)行【第五步】 - 沒有找到撤嫩,執(zhí)行【第六步】
- 如果找到,
-
【第六步】執(zhí)行對象方法
valueForUndefinedKey :
,默認拋出NSUndefinedKeyException
的異常
KVC使用場景
1蠢终、動態(tài)設值和取值
- 通過
setValue:forKey:
和valueForKey:
- 通過
路由
的方式setValue:forKeyPath:
和valueForKeyPath:
2序攘、訪問和修改私有變量
對于類的私有屬性
,在外部定義的對象寻拂,是無法直接訪問私有屬性的程奠,但是對于KVC而言,一個對象沒有自己的隱私祭钉,可以通過KVC修改和訪問任何私有屬性
3瞄沙、多值操作(model和字典互轉(zhuǎn))
model和字典的轉(zhuǎn)換可以通過下面兩個KVC的API實現(xiàn)
//字典轉(zhuǎn)模型
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
//模型轉(zhuǎn)字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
4、修改一些系統(tǒng)的內(nèi)部屬性
很多UI控件并沒有提供訪問的API,但是使用KVC可以解決這個問題距境,自定義tabbar
申尼、個性化UITextField中
的placeHolderText
5、高階消息傳遞
在對容器類使用KVC
時垫桂,valueForKey:將會被傳遞給容器中的每一個對象
师幕,而不是對容器本身進行操作,結(jié)果會被添加到返回的容器中诬滩,這樣霹粥,可以很方便的操作集合 來返回 另一個集合
//KVC實現(xiàn)高階消息傳遞
- (void)transmitMsg{
NSArray *arrStr = @[@"english", @"franch", @"chinese"];
NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];//首字母大寫
for (NSString *str in arrCapStr) {
NSLog(@"%@", str);
}
NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber *length in arrCapStrLength) {
NSLog(@"%ld", (long)length.integerValue);
}
}
//********打印結(jié)果********
2020-10-27 11:33:43.377672+0800 CJLCustom[60035:6380757] English
2020-10-27 11:33:43.377773+0800 CJLCustom[60035:6380757] Franch
2020-10-27 11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese
2020-10-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7
2020-10-27 11:33:43.378327+0800 CJLCustom[60035:6380757] 6
2020-10-27 11:33:43.378417+0800 CJLCustom[60035:6380757] 7
自定義KVC
自定義KVC設值
- 1、判斷
key是否為空
- 2疼鸟、查找
setter
方法后控,set<Key>、_set<Key>愚臀、 setIs<Key>
- 3忆蚀、判斷是否能間接訪問實例變量,即是否響應
accessInstanceVariablesDirectly
方法姑裂,- YES 繼續(xù)下一步
- NO 崩潰報錯
- 4馋袜、間接訪問變量賦值(只會走一次)
_key、_isKey舶斧、key欣鳖、isKey
- 4.1、定義一個收集實例變量的集合
- 4.2茴厉、
class_getInstanceVariable
獲取對應的ivar(實例變量) - 4.3泽台、
object_setIvar
,設置對應的ivar(實例變量)值
- 5矾缓、如果找不到相關(guān)實例變量怀酷,則拋出異常
自定義KVC取值
- 1、判斷
Key空值
- 2嗜闻、查找對應的方法
get<kKey>蜕依、<Key>、countof<Key>琉雳、objectIn<Key>AtIndex
- 3样眠、判斷是否能間接賦值實例變量,即判斷是否響應
accessInstanceVariablesDirectly
方法- YES 繼續(xù)下一步
- NO 崩潰報錯
- 4翠肘、間接訪問實例變量檐束,
_<key> _is<Key> <key> is<Key>
- 4.1 定義一個收集實例變量的可變數(shù)組
- 4.2
class_getInstanceVariable
,獲取相應的 ivar - 4.3
object_getIvar
束倍,返回相應的 ivar 的值
keyPath -- 路由訪問
//通過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通過KeyPath來設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
//CJLPerson類
@interface CJLPerson : NSObject
@property (nonatomic, copy) NSString *age;
@property (nonatomic, strong) CJLStudent *student;
@end
//CJLStudent類
@interface CJLStudent : NSObject
@property (nonatomic, copy) NSString *name;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
CJLPerson *person = [[CJLPerson alloc] init];
CJLStudent *student = [CJLStudent alloc];
student.name = @"CJL";
person.student = student;
//根據(jù)kvc - keyPath路由修改student的subject屬性的值
[person setValue:@"嘻嘻" forKeyPath:@"student.name"];
NSLog(@"%@",[person valueForKeyPath:@"student.name"]);
}
return 0;
}
//*************打印結(jié)果*************
2020-10-27 09:55:08.512833+0800 001-KVC簡介[58089:6301894] 改變前:CJL
2020-10-27 09:55:08.512929+0800 001-KVC簡介[58089:6301894] 改變后:嘻嘻
自定義KVC代碼
#import "NSObject+YPKVC.h"
#import <objc/runtime.h>
@implementation NSObject (YPKVC)
//設值
- (void)yp_setValue:(nullable id)value forKey:(NSString *)key{
// 1被丧、判斷key 是否存在
if (key == nil || key.length == 0) return;
// 2盟戏、找setter方法,順序是:setXXX晚碾、_setXXX抓半、 setIsXXX
// key 要大寫
NSString *Key = key.capitalizedString;
// key 要大寫
NSString *setKey = [NSString stringWithFormat:@"set%@:", Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key];
if ([self yp_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*************%@*************", setKey);
return;
}else if([self yp_performSelectorWithMethodName:_setKey value:value]){
NSLog(@"*************%@*************", _setKey);
return;
}else if([self yp_performSelectorWithMethodName:setIsKey value:value]){
NSLog(@"*************%@*************", setIsKey);
return;
}
// 3、判斷是否響應`accessInstanceVariablesDirectly`方法格嘁,即間接訪問實例變量笛求,返回YES,繼續(xù)下一步設值糕簿,如果是NO探入,則崩潰
if (![self.class accessInstanceVariablesDirectly]) {
@throw [NSException exceptionWithName:@"YPUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4、間接訪問變量賦值懂诗,順序為:_key蜂嗽、_isKey、key殃恒、isKey
// 4.1 定義一個收集實例變量的可變數(shù)組
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
NSString *_key = [NSString stringWithFormat:@"_%@", key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@", key];
NSString *isKey = [NSString stringWithFormat:@"is%@", key];
if ([mArray containsObject:_key]) {
// 4.2 獲取相應的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 對相應的 ivar 設置值
object_setIvar(self, ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self, ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self, ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self, ivar, value);
return;
}
// 5植旧、如果找不到則拋出異常
@throw [NSException exceptionWithName:@"YPUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
//取值
- (nullable id)yp_valueForKey:(NSString *)key{
// 1、判斷非空
if (key == nil || key.length == 0) {
return nil;
}
// 2离唐、找到相關(guān)方法:get<Key> <key> countOf<Key> objectIn<Key>AtIndex
// key 要大寫
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}
//集合類型
else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
// 3病附、判斷是否響應`accessInstanceVariablesDirectly`方法,即間接訪問實例變量完沪,返回YES熟呛,繼續(xù)下一步設值,如果是NO馋没,則崩潰
if (![self.class accessInstanceVariablesDirectly]) {
@throw [NSException exceptionWithName:@"YPUnKnownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.找相關(guān)實例變量進行賦值婆排,順序為:_<key>鉴扫、 _is<Key>、 <key>、 is<Key>
// 4.1 定義一個收集實例變量的可變數(shù)組
NSMutableArray *mArray = [self getIvarListName];
// 例如:_name -> _isName -> name -> isName
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
return @"";
return @"";
}
#pragma mark - 相關(guān)方法
- (BOOL)yp_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
//如果你確定不會發(fā)生內(nèi)存泄漏的情況下姥饰,可以使用如下的語句來忽略掉這條警告
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
return YES;
}
return NO;
}
- (id)performSelectorWithMethodName:(NSString *)methodName{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
//如果你確定不會發(fā)生內(nèi)存泄漏的情況下岂座,可以使用如下的語句來忽略掉這條警告
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
}
return nil;
}
- (NSMutableArray *)getIvarListName{
//創(chuàng)建可變數(shù)組钾恢,用于存儲ivar成員變量
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
unsigned int count = 0;
//獲取類的成員變量列表
Ivar *ivars = class_copyIvarList([self class], &count);
//遍歷成員變量列表
for (int i = 0; i<count; i++) {
Ivar ivar = ivars[i];
//獲取成員變量名字字符
const char *ivarNameChar = ivar_getName(ivar);
//將字符轉(zhuǎn)換成字符串
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@", ivarName);
//存入可變數(shù)組
[mArray addObject:ivarName];
}
//釋放成員變量列表
free(ivars);
return mArray;
}
@end