目錄
相關(guān)面試題:談?wù)勀銓VC的理解
KVC的全稱是Key-Value Coding,翻譯成中文是鍵值編碼,鍵值編碼是由NSKeyValueCoding非正式協(xié)議啟用的一種機制,對象采用該協(xié)議來間接訪問其屬性。即可以通過一個字符串key來訪問某個屬性。這種間接訪問機制補充了實例變量及其相關(guān)的訪問器所提供的直接訪問。
一、KVC基本使用
// 1:基本類型使用
NAPerson *person = [NAPerson new];
[person setValue:@"differ" forKey:@"name"];
CJLog(@"%@",person.name);
// 2:集合類型使用
person.array = @[@"1",@"2",@"3"];
//person.array[0] = @"100"; 不能直接修改數(shù)組中的元素
// 方式一:創(chuàng)建一個新的數(shù)組
NSArray *array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
CJLog(@"%@",[person valueForKey:@"array"]);
// 方式二:構(gòu)建可變數(shù)組
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
CJLog(@"%@",[person valueForKey:@"array"]);
// 3:集合操作符使用
// 遍歷
NSEnumerator *enumerator = [array objectEnumerator];
NSString *str = nil;
while (str = [enumerator nextObject]) {
CJLog(@"%@", str);
}
// 字典操作
NSDictionary *dict = @{@"name":@"differ",@"subject":@"iOS",};
NAStudent *p = [[NAStudent alloc] init];
// 字典轉(zhuǎn)模型
[p setValuesForKeysWithDictionary:dict];
// 鍵數(shù)組轉(zhuǎn)模型到字典
NSArray *keyArray = @[@"name",@"subject"];
NSDictionary *dic = [p dictionaryWithValuesForKeys:keyArray];
// 聚合操作符 @avg、@count、@max、@min、@sum
// 數(shù)組操作符 @distinctUnionOfObjects @unionOfObjects
// 嵌套集合(array&set)操作 @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets
// 4:訪問非對象屬性 - 面試可能問到
// 結(jié)構(gòu)體-官方文檔示例
ThreeFloats floats = {1.,2.,3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *value1 = [person valueForKey:@"threeFloats"];
CJLog(@"%@",value1);
ThreeFloats th;
[value1 getValue:&th];
CJLog(@"%f-%f-%f",th.x,th.y,th.z);
// 5:KVC - 層層訪問 - keyPath
NAStudent *student = [NAStudent alloc];
student.subject = @"differ";
person.student = student;
[person setValue:@"Swift" forKeyPath:@"student.subject"];
CJLog(@"%@",[person valueForKeyPath:@"student.subject"]);
typedef struct {
float x, y, z;
} ThreeFloats;
進入setValue:forKey:方法我們可以看到該方法在NSObject的NSKeyValueCoding分類中,因此所有繼承于NSObject的類均有KVC功能。

官方文檔地址:Key-Value Coding Programming Guide
二、KVC設(shè)值、取值底層分析
在官方文檔Search Pattern for the Basic Setter描述中可以看到KVC的設(shè)值過程。
Search Pattern for the Basic Setter
The default implementation of setValue:forKey:, given key and value parameters as input, attempts to set a property named key to value (or, for non-object properties, the unwrapped version of value, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:
Look for the first accessor named
set<Key>:or_set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.If no simple accessor is found, and if the class method
accessInstanceVariablesDirectlyreturnsYES, look for an instance variable with a name like_<key>,_is<Key>,<key>, oris<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.Upon finding no accessor or instance variable, invoke
setValue:forUndefinedKey:. This raises an exception by default, but a subclass ofNSObjectmay provide key-specific behavior.
根據(jù)官方文檔可以繪制如下設(shè)值流程圖:

- 設(shè)值方法的順序為:
setName->_setName->setIsName - 尋找變量的順序為:
_name->_isName->name->isName(沒有定義前面的成員變量才會向后尋找)
取值方法:[person valueForKey@"name"];
- 取值方法的順序為:
getName->name->isName->_name - 尋找變量的順序為:
_name->_isName->name->isName
三、簡單自定義KVC設(shè)值、取值
這里通過簡單自定義KVC設(shè)值、取值加深對KVC底層機制的理解
自定義KVC設(shè)置流程,主要分為以下幾個步驟:
- 判斷
key非空 - 查找
setter方法,順序是:setKey、_setKey、 setIsKey - 判斷
accessInstanceVariablesDirectly方法的返回值,即是否允許間接訪問成員變量
3.1 返回YES,繼續(xù)下一步設(shè)值,
3.2 返回NO,拋出異常 - 間接訪問變量賦值(只會走一次),順序是:
_key、_isKey、key、isKey
4.1 定義一個收集實例變量的數(shù)組
4.2 通過class_getInstanceVariable方法,獲取相應(yīng)的ivar
4.3 通過object_setIvar方法,對相應(yīng)的ivar設(shè)置值 - 如果找不到相關(guān)實例變量,則拋出異常
//設(shè)值
- (void)lcj_setValue:(nullable id)value forKey:(NSString *)key{
// 1、判斷key 是否存在
if (key == nil || key.length == 0) return;
// 2、找setter方法,順序是:setKey、_setKey、 setIsKey
// 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 lcj_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*************%@*************", setKey);
return;
} else if([self lcj_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*************%@*************", _setKey);
return;
} else if([self lcj_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*************%@*************", setIsKey);
return;
}
// 3、判斷accessInstanceVariablesDirectly方法的返回值,即是否允許間接訪問成員變量,返回YES,繼續(xù)下一步設(shè)值,如果是NO,則拋出異常
if (![self.class accessInstanceVariablesDirectly]) {
@throw [NSException exceptionWithName:@"CJLUnKnownKeyException" 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 獲取相應(yīng)的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 對相應(yīng)的 ivar 設(shè)置值
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:@"LCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
自定義KVC取置流程,主要分為以下幾個步驟:
- 判斷
key非空 - 查找相應(yīng)方法,順序是:
get<Key>、<key>、is<Key>、 _<key>(NSArray還會尋找countOf<Key> and objectIn<Key>AtIndex) - 判斷
accessInstanceVariablesDirectly方法的返回值,即是否允許間接訪問成員變量
3.1 返回YES,繼續(xù)下一步設(shè)值,
3.2 返回NO,拋出異常 - 間接訪問實例變量,順序依然是:
_<key>、 _is<Key>、 <key>、 is<Key>
4.1 定義一個收集實例變量的數(shù)組
4.2 通過class_getInstanceVariable方法,獲取相應(yīng)的ivar
4.3 通過object_getIvar方法,返回相應(yīng)的ivar的值 - 如果找不到相關(guān)實例變量,返回空字符串
//取值
- (nullable id)lcj_valueForKey:(NSString *)key {
// 1、判斷非空
if (key == nil || key.length == 0) {
return nil;
}
// 2、找到相關(guān)方法:get<Key>、 <key> 、is<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 (is<Key>、 _<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ù)下一步設(shè)值,如果是NO,則拋出異常
if (![self.class accessInstanceVariablesDirectly]) {
@throw [NSException exceptionWithName:@"CJLUnKnownKeyException" 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 @"";
}
四、KVC 使用場景
1、動態(tài)設(shè)值和取值
- 常用的可以通過
setValue:forKey:和valueForKey: - 也可以通過路由的方式
setValue:forKeyPath:和valueForKeyPath:
2、通過KVC訪問和修改私有變量
在日常開發(fā)中,對于類的私有屬性,在外部定義的對象,是無法直接訪問私有屬性的,但是對于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)部屬性
在日常開發(fā)中,我們知道,很多UI控件都是在其內(nèi)部由多個UI空間組合而成,這些內(nèi)部控件蘋果并沒有提供訪問的API,但是使用KVC可以解決這個問題,常用的就是自定義tabbar、個性化UITextField中的placeHolderText(iOS13開始逐步不允許通過valueForKey、setValue: forKey獲取和設(shè)置私有屬性,如:_placeholderLabel.textColor)。
5、用KVC實現(xiàn)高階消息傳遞
在對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每一個對象,而不是對容器本身進行操作,結(jié)果會被添加到返回的容器中,這樣可以很方便的操作集合 來返回 另一個集合
//KVC實現(xiàn)高階消息傳遞
- (void)transmitMsg{
NSArray *arrStr = @[@"english", @"franch", @"chinese"];
NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString *str in arrCapStr) {
CJLog(@"%@", str);
}
NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber *length in arrCapStrLength) {
CJLog(@"%ld", (long)length.integerValue);
}
}
// 打印結(jié)果:
English
Franch
Chinese
7
6
7