1.KVC protocol definition
Key-value encoding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects use to provide indirect access to their properties. When an object conforms to a key-value encoding, its properties can be addressed via a concise, uniform messaging interface via string parameters. This indirect access mechanism complements the direct access provided by instance variables and their associated accessor methods.
-
KVC is defined in Objective-C
KVC is defined as an extension of NSObject (there is an explicit NSKeyValueCoding class name – classification in Objective-C). If you look at the setValueForKey method, you can find it in Foundation. Foundation framework is not open source, you can only find it in the Official Apple documentation. See below:
2. API methods provided by KVC
-
We can learn to read apple’s official documentation to gain a deeper understanding of KVC.
Key-Value Coding Programming Guide
Apple has a special implementation for some container classes like NSArray or NSSet, KVC.
-
Commonly used method
For all types that inherit NSObject, which means that almost all Objective-C objects can use KVC, here are the four most important methods of KVC:
- (nullable id)valueForKey:(NSString *)key; - (void)setValue:(nullable id)value forKey:(NSString *) Key; // set the value by Key - (nullable id)valueForKeyPath:(NSString *)keyPath; // set the value to KeyPath. - (void)setValue:(nullable id)value forKeyPath:(NSString *) KeyPath; // set the value to KeyPathCopy the code
-
Special methods
Of course, there are other methods in the NSKeyValueCoding category, and those methods will be used when you have special cases or special requirements.
/ / the default returns YES, said if the Set method is not found, will be in accordance with the _key, _iskey, key, iskey sequential search members, Set to NO don't search + (BOOL) accessInstanceVariablesDirectly; // KVC provides an API for verifying the correctness of attribute values, which can be used to check if the value of a set is correct, make a replacement value for an incorrect value, or reject a new value and return the cause of the error. - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; // This is the set operation API, there are a series of such apis, if the property is an NSMutableArray, then you can use this method to return. - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; // If the Key does not exist and no KVC can find any fields or attributes related to the Key, this method is called. By default, an exception is thrown. - (nullable id)valueForUndefinedKey:(NSString *)key; // Same as the previous method, but this method is set. - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; // if you pass nil to Value during the SetValue method, this method is called - (void)setNilValueForKey:(NSString *)key; // Input a set of keys, return the values corresponding to the keys, and then return the dictionary, which is used to transfer the Model to the dictionary. - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;Copy the code
-
Structural treatment
When KVC conducts structure processing, it needs to use NSValue. When setting the value, it encapsulates the structure into NSValue and sets the key value. The value also returns an NSValue, which is then parsed as a structure, as shown in the following code:
Floats = {1.,2.,3.}; // Encapsulate NSValue NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)]; // set the value [person setValue:value forKey:@"threeFloats"]; // Value NSValue *value1 = [person valueForKey:@"threeFloats"]; // Struct parse ThreeFloats th; [value1 getValue:&th]; NSLog(@"%f-%f-%f",th.x,th.y,th.z);Copy the code
-
Dictionary processing (model transformation)
Dictionaries can be swapped with models or retrieved from models using key-value arrays. See the following code:
- (void) dictionaryTest NSDictionary * dict = @ {{/ / dictionary @ "name:" @ "Cooci," @ "Nick:" @ "KC" @ "subject:" @ "iOS," @ "age" : @ 18, @"length":@180 }; LGStudent *p = [[LGStudent alloc] init]; / / dictionary model [p setValuesForKeysWithDictionary: dict]; NSArray *array = @[@"name",@"age"]; / / get a response from the model of the data dictionary NSDictionary * dic = [p dictionaryWithValuesForKeys: array]; NSLog(@"%@",dic); }Copy the code
3.KVC sets the value sequence
We all know how KVC is used, but in what order does KVC internally look for keys? That’s what we’re going to explore.
1. Set a value
What is the underlying execution mechanism when the setValue:forKey: code is called? There are relevant instructions in the official documents, as shown below:
-
Translation:
The default implementation of setValue:forKey: attempts to set a property named key to value, given key and value parameters as input, using the following procedure inside the object receiving the call: Find the first accessor named set< key >: or _set< key > in order. If found, it is called with the input value (or expanded as needed) and done. If you do not find simple accessor, and class methods accessInstanceVariablesDirectly returns YES, is in order to find the name similar to _ < key >, _is < key >, < the key > or is < key > instance variables. If found, set the variable directly with the input value (or unpack value) and finish. When the visitor was not found or instance variables, called setValue: forUndefinedKey:. By default, this throws an exception, but subclasses of NSObject may provide key-specific behavior.
-
According to the official content above, the implementation mechanism can be concluded as follows:
- Search for names in order
set<Key>
._set<Key>
orsetIs<Key>
thesetter
The accessor looks for it sequentially and calls it if it finds it - As long as either one is implemented, this method is called to set the value of the property to the value passed in
- If you don’t find these
setter
Method,KVC
Mechanism checks+ (BOOL)accessInstanceVariablesDirectly
Does the method returnYES
By default, this method returnsYES
If the method is overridden to returnNO
So, in this stepKVC
Will performSetValue: forUndefinedKey:
Methods; - If the return
YES
.KVC
The mechanism will preferentially search for names in the class_<Key>
, regardless of whether the variable is defined at the interface of the class or at the implementation of the class, and regardless of what access modifier is used, only exists when_<Key>
Named variable,KVC
Can be assigned to the member variable KVC
Mechanism will continue to search_is<Key>
,<key>
andis<key>
, and assign values to them- If none of the methods or member variables listed above exist, the system will execute the object
SetValue: forUndefinedKey:
Method, which by default throws an exception.
- Search for names in order
-
In [person setValue: @ “newName” forKey: @ “name”]. For example, the following conclusions can be drawn:
-
Setter methods are preferred for property setting, in the order of invocation:
setName
_setName
setIsName
-
If the above methods are not found and accessInstanceVariablesDirectly returns YES, by a member variable set, the order is:
_name
_isName
name
isName
This can be verified by a case, which is not shown here.
-
-
AccessInstanceVariablesDirectly instructions
Rewrite + (BOOL) accessInstanceVariablesDirectly ways to make its return NO, in that case, if the KVC had not found the set < Key >, _set < Key >, setIs < Key > related method, can directly use setValue: ForUndefinedKey: method. Let’s test the above KVC mechanism with code:
@interface LGPerson : NSObject { @public NSString *_isName; NSString *name; NSString *isName; NSString *_name; } @end @implementation LGPerson +(BOOL)accessInstanceVariablesDirectly{ return NO; } -(id)valueForUndefinedKey:(NSString *)key{NSLog(@" exception occurred, the key does not exist %@",key); return nil; } -(void)setValue:(id)value forUndefinedKey:(NSString *)key{NSLog(@" error, the key does not exist %@",key); // -(void)setName:(NSString*)name{// toSetName = name; // } // - (void)_setName:(NSString *)name{ // NSLog(@"%s - %@",__func__,name); // } // - (void)setIsName:(NSString *)name{ // NSLog(@"%s - %@",__func__,name); // } @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... LGPerson* person = [LGPerson new]; [person setValue:@"NewName" forKey:@"name"]; NSString* name = [person valueForKey:@"name"]; NSLog(@"value for key : %@",name); NSLog (@ "_name values: % @", the person - > _name); NSLog (@ "_isName values: % @", the person - > _isName); NSLog (@ "name: value % @", the person - > name); NSLog (@ "isName values: % @", the person - > isName); } return 0; }Copy the code
The operation structure is shown below:
This illustrates the rewrite + (BOOL) accessInstanceVariablesDirectly ways to make its return after NO KVC can’t find the set < Key > method, such as NO longer to find the Key > series of member variables, but direct call setValue: forUndefinedKey: Method, so developers who don’t want their classes to implement KVC can do so.
-
KVC setting flowchart
Value of 2.
What is the underlying execution mechanism when calling the code for valueForKey:? There are relevant instructions in the official documents, as shown below:
-
According to the official content above, the following implementation mechanism can be obtained after translation:
- First of all, according to the
get<Key>
.<Key>
.is<Key>
._<Key>
The sequential method lookup ofgetter
Method will be called if it is found, if it isBOOL
orInt
The equivalent type will be wrapped as aNSNumber
Object. - If the top
getter
I couldn’t find it.KVC
Will findcountOf<Key>
.objectIn<Key>AtIndex
or<Key>AtIndexes
Format methods. ifcountOf<Key>
Method and one of the other two methods are found, and a ready response is returnedNSArray
A collection of proxies for all methods (it isNSKeyValueArray
, it isNSArray
Subclass), calls the methods of the proxy collection, or sends a class ofNSArray
“, will be tocountOf<Key>
.objectIn<Key>AtIndex
orAt<Key>Indexes
Call in the form of a combination of these methods. There’s another optionget<Key>:range:
Methods. So you want to redefine itKVC
You can add these methods to the list, but you need to make sure that your method names matchKVC
Standard naming methods, including method signatures. - If the above methods are not found, both are searched
countOf<Key>
.enumeratorOf<Key>
.memberOf<Key>
Format methods. If all three methods are found, a valid response is returnedNSSet
The proxy collection of all methods, as above, is sent to this proxy collectionNSSet
“, will be withcountOf<Key>
.enumeratorOf<Key>
.memberOf<Key>
Call in the form of a combination. - If not, check the class method again
+ (BOOL)accessInstanceVariablesDirectly
, if returnYES
(default behavior), then, as previously set, will press_<Key>
._is<Key>
.<Key>
.is<Key>
This is not recommended because direct access to instance variables breaks encapsulation and makes the code more vulnerable. If you override the class method+ (BOOL)accessInstanceVariablesDirectly
returnNO
If so, it will be called directlyvalueForUndefinedKey:
- If not found, call
valueForUndefinedKey:
- First of all, according to the
-
In [person valueForKey: @ “name”]. As an example
-
Getter methods are called in the following order:
getName
name
isName
_name
-
If the above method is not found, accessInstanceVariablesDirectly returns YES, directly to return to the member variable and obtain order is:
_name
_isName
name
isName
This can be verified by a case, which is not shown here.
-
-
KVC value flow chart
You can verify the above conclusion with the following code!
@interface LGPerson : NSObject { @public NSString *_isName; NSString *name; NSString *isName; NSString *_name; } @end @implementation LGPerson +(BOOL)accessInstanceVariablesDirectly{ return NO; } -(id)valueForUndefinedKey:(NSString *)key{NSLog(@" exception occurred, the key does not exist %@",key); return nil; } -(void)setValue:(id)value forUndefinedKey:(NSString *)key{NSLog(@" error, the key does not exist %@",key); // -(void)setName:(NSString*)name{// toSetName = name; // } // - (void)_setName:(NSString *)name{ // NSLog(@"%s - %@",__func__,name); // } // - (void)setIsName:(NSString *)name{ // NSLog(@"%s - %@",__func__,name); GetName {// return NSStringFromSelector(_cmd); //} //- (NSString *)name{ // return NSStringFromSelector(_cmd); //} //- (NSString *)isName{ // return NSStringFromSelector(_cmd); //} //- (NSString *)_name{ // return NSStringFromSelector(_cmd); //} @end int main(int argc, const char * argv[]) { @autoreleasepool { // insert code here... LGPerson* person = [LGPerson new]; [person setValue:@"NewName" forKey:@"name"]; NSString* name = [person valueForKey:@"name"]; NSLog(@"value for key : %@",name); NSLog (@ "_name values: % @", the person - > _name); NSLog (@ "_isName values: % @", the person - > _isName); NSLog (@ "name: value % @", the person - > name); NSLog (@ "isName values: % @", the person - > isName); } return 0; }Copy the code
4. Use keyPath in KVC
In addition to assigning to the properties of the current object, you can also assign to objects deeper in the object. For example, assign to the country property of the location property of the current object. KVC performs multi-level access directly using point syntax similar to attribute calls.
[person setValue:@"" forKeyPath:@"location.country"]; Copy the codeCopy the code
When you value an array using keyPath and store objects of the same type, you can use the valueForKeyPath: method to specify a field for all the objects in the array. For example, in the following example, valueForKeyPath: takes the name property values of all objects in the array and returns them in an array.
NSArray *names = [array valueForKeyPath:@"name"]; Copy the codeCopy the code
5. Exception handling
If the corresponding key or keyPath cannot be found according to the KVC search rules, the corresponding exception method is invoked. The default implementation of exception methods that throw an exception when an exception occurs and Crash the application. See below:
We can override the following two methods:
-(id)valueForUndefinedKey:(NSString *)key{NSLog(@" exception occurred, the key does not exist %@",key); return nil; } -(void)setValue:(id)value forUndefinedKey:(NSString *)key{NSLog(@" error, the key does not exist %@",key); }Copy the code
After overriding these two methods, the running program no longer crashes, as shown below:
However, exceptions caused by KVC can be properly handled according to service requirements. For example:
- (void)setNilValueForKey:(NSString *)key {if ([key isEqualToString:@"name"]) {[self setValue:@"" forKey:@ "age "]; } else { [super setNilValueForKey:key]; }}Copy the code
6. Customize KVC
You can customize KVC by yourself according to the setting and value rules provided in the Official Apple documentation. See the implementation code below:
// KVC 自定义
@implementation NSObject (LGKVC)
// 设置
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
// 1: 判断什么 key
if (key == nil || key.length == 0) {
return;
}
// 2: setter set<Key>: or _set<Key>,
// key 要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
// 是否存在方法
if ([self lg_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
// 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
// 3:判断是否能够直接赋值实例变量——NO
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4: 间接变量
// 获取 ivar -> 遍历 containsObjct -
// 4.1 定义一个收集实例变量的可变数组
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:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
// 取值
- (nullable id)lg_valueForKey:(NSString *)key{
// 1:刷选key 判断非空
if (key == nil || key.length == 0) {
return nil;
}
// 2:找到相关方法 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:判断是否能够直接赋值实例变量-YES、NO
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// _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 @"";
}
#pragma mark **- 相关方法**
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#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
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
}
return nil;
}
- (NSMutableArray *)getIvarListName{
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);
NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
NSLog(@"ivarName == %@",ivarName);
[mArray addObject:ivarName];
}
free(ivars);
return mArray;
}
@end
Copy the code
This paper collected