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 orderset<Key>._set<Key>orsetIs<Key>thesetterThe 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 thesesetterMethod,KVCMechanism checks+ (BOOL)accessInstanceVariablesDirectlyDoes the method returnYESBy default, this method returnsYESIf the method is overridden to returnNOSo, in this stepKVCWill performSetValue: forUndefinedKey:Methods;
    • If the returnYES.KVCThe 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,KVCCan be assigned to the member variable
    • KVCMechanism 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 objectSetValue: forUndefinedKey:Method, which by default throws an exception.
  • 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:

      1. setName
      2. _setName
      3. setIsName
    • If the above methods are not found and accessInstanceVariablesDirectly returns YES, by a member variable set, the order is:

      1. _name
      2. _isName
      3. name
      4. 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 theget<Key>.<Key>.is<Key>._<Key>The sequential method lookup ofgetterMethod will be called if it is found, if it isBOOLorIntThe equivalent type will be wrapped as aNSNumberObject.
    • If the topgetterI couldn’t find it.KVCWill findcountOf<Key>.objectIn<Key>AtIndexor<Key>AtIndexesFormat methods. ifcountOf<Key>Method and one of the other two methods are found, and a ready response is returnedNSArrayA collection of proxies for all methods (it isNSKeyValueArray, it isNSArraySubclass), calls the methods of the proxy collection, or sends a class ofNSArray“, will be tocountOf<Key>.objectIn<Key>AtIndexorAt<Key>IndexesCall in the form of a combination of these methods. There’s another optionget<Key>:range:Methods. So you want to redefine itKVCYou can add these methods to the list, but you need to make sure that your method names matchKVCStandard naming methods, including method signatures.
    • If the above methods are not found, both are searchedcountOf<Key>.enumeratorOf<Key>.memberOf<Key>Format methods. If all three methods are found, a valid response is returnedNSSetThe 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)accessInstanceVariablesDirectlyreturnNOIf so, it will be called directlyvalueForUndefinedKey:
    • If not found, callvalueForUndefinedKey:
  • In [person valueForKey: @ “name”]. As an example

    • Getter methods are called in the following order:

      1. getName
      2. name
      3. isName
      4. _name
    • If the above method is not found, accessInstanceVariablesDirectly returns YES, directly to return to the member variable and obtain order is:

      1. _name
      2. _isName
      3. name
      4. 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