Key-value Coding (KVC) is a mechanism enabled by the NSKeyValueCoding informal protocol that objects use to access their properties indirectly, that is, they can access an attribute through a string Key. This indirect access mechanism complements the direct access provided by instance variables and their associated accessor methods.

KVC commonly used API

  • throughkeySet the value/value
// Use Key directly
- (nullable id)valueForKey:(NSString *)key;

// Set the value by Key
- (void)setValue:(nullable id)value forKey:(NSString *)key;
Copy the code
  • throughkeyPath(i.e.routing) Set the value/value
// The value can be KeyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath; 

// set the value to KeyPath
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  
Copy the code

Other methods

If Set
      
        is not found, members will be searched in the order _key, _iskey, Key, and iskey
      
+ (BOOL)accessInstanceVariablesDirectly;

It can be used to check that the set value is correct, to make a replacement value for an incorrect value, or to 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 KVC cannot 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 set SetValue to nil for a basic data type such as' int 'or' float ', it triggers a call to this method and you can do exception handling or reassign keys in this method
- (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

KVC set value underlying principle

In daily development, there are two ways to assign object attributes

  • Directly throughsetterMethods the assignment
  • throughKVC key value encodingThe related API assignment of
LGPerson *person = [[LGPerson alloc] init];
// 1. General setter methods
person.name      = @"Lg_ ha ha";
// 2
[person setValue:@"Lg_ hee hee" forKey:@"name"]; 
Copy the code

The following is to explore the underlying principle of the most used KVC value setting method: setValue:forKey.

The setValue:forKey declaration is found in the Foundation framework, which is not open source. There are several ways to explore the underlying layer

  • throughHopperDisassemble, look at pseudocode
  • By appleThe official documentation
  • GithubSearch for relevant demos. For exampleDIS_KVC_KVO

Here, we studied the key-value Coding Programming Guide from apple’s official document, and the Value setting process is described as follows

When setValue:forKey: sets the property value, the underlying execution flow is as follows:

  • Set

    -> _set

    -> setIs >

    • If there is one of themAny setter method, is called in this methodSet the value of the key(note:Key is the name of a member variable, the first character case must conform to KVC naming conventions.
    • If allThere is no, then enter [Step 2]
  • 】 【 the second step: if you don’t have the first step of three simple setter method, is to find whether + (BOOL) accessInstanceVariablesDirectly returns YES

    • If YES is returned, the indirectly accessed instance variables are assigned in the following order: _

      -> _is< key>

      • If any of the instance variables are found, the value is assigned
      • If none, go to step 3.
    • If NO is returned, go to step 3.

  • If neither setter method nor instance variable is found, the system executes the setValue: forUndefinedKey: method of the object, throwing an exception of type NSUndefinedKeyException by default.

To sum up,KVC sets the value using setValue:forKey:The process of setting the attribute name of the object person of LGPerson is taken as an example, as shown in the figure below:

Underlying principles of KVC value

Similarly, we can analyze the underlying principle of KVC value through official documents.

When valueForKey: is called, the underlying execution flow is as follows

  • Get

    ->

    -> is

    -> _< Key>


    • iffind, then enter [Step 5]
    • ifCould not find, Array Set type goes to step 2, Set Set type goes to Step 3, and others go to Step 4.
  • If the getter in step 1 is not found, KVC will look for countOf

    and objectIn

    AtIndex: and

    AtIndexes:


    • If one of countOf

      and the other two is found, a collection proxy object that responds to all NSArray methods is created and returned, NSKeyValueArray, which is a subclass of NSArray. The proxy object then converts all the NSArray messages it receives into some combination of countOf

      , objectIn

      AtIndex:, and

      AtIndexes: messages to create key-value encoded objects. If the original object also implements an optional method called get

      : range:, then the proxy object will use that method as appropriate (note: method names are named according to KVC’s standard naming conventions, including method signatures).




    • If not, proceed to step 4 if these three access the array.

  • If none of the above methods are found, the countOf

    , enumeratorOf

    , and memberOf

    methods are searched simultaneously


    • If all three methods are found, a response is createdA collection of proxy objects for all NSSet methodsAnd returns the object, which the proxy object then receives allNSSetMessage conversion toCountOf <Key>, enumeratorOf<Key> and memberOf<Key> :Some combination of messages used to create its objects
    • If still not found, go to [Step 4]
  • If you haven’t found, the fourth step 】 【 check method accessInstanceVariablesDirectly YES, search in turn _ < key >, _is < key >, < the key > or is < key > instance variables

    • If I find it,Gets the value of the instance variable directly, enter [Step 5]
  • [Step 5] Return different results according to the type of attribute value searched

    • If it isPointer to the object, the result is returned directly
    • If it isNSNumber supportThe scalar type of theStored in an NSNumber instanceAnd return it
    • If is wereNSNumber does not supportScalar type, pleaseTo an NSValue objectAnd returns the object
  • [Step 6] If the above five methods fail, the system executes the valueForUndefinedKey: method of the object, which by default throws an exception of type NSUndefinedKeyException

To sum up, KVC value process through valueForKey: method takes setting the attribute name of LGPerson object person as an example, as shown in the figure below:

KVC assignment and value example source code

Use route access, known as keyPath

, in the daily development of a class member variables can be custom class or other complex data types, operation is commonly, we can get the first through the KVC attributes, and then through the KVC get custom attributes of a class, is more troublesome, there is another more simple method, is to use the KeyPath routing, involves the following two methods: SetValue: forKeyPath: and valueForKeyPath:

// The value can be KeyPath
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  

// set the value to KeyPath
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  
Copy the code

Consider the following examples:

/ / CJLPerson class
@interface CJLPerson : NSObject
@property (nonatomic, copy)   NSString          *age;
@property (nonatomic, strong) CJLStudent         *student;
@end

/ / CJLStudent class
@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;
        // Change the value of student's subject property based on the kvC-keypath route
        [person setValue:@"Hee hee" forKeyPath:@"student.name"];
        NSLog(@"% @",[person valueForKeyPath:@"student.name"]);
    }
    return 0;
}

//************* The result is printed *************
2020-10-27 09:55:08.512833+0800 001- introduction of KVC [58089:6301894[before: CJL2020-10-27 09:55:08.512929+0800 001- introduction of KVC [58089:6301894[after change: hee heeCopy the code

Custom KVC

Custom KVC source code

Implement custom lg_setValue:forKey: and lg_valueForKey: methods by adding the classification LGKVC to NSObject, according to the lookup rules provided in the Official Apple documentation

@interface NSObject (LGKVC)

/ / set value
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key;
/ / value
- (nullable id)lg_valueForKey:(NSString *)key;

@end
Copy the code

Custom KVC setting

The process for customizing KVC Settings is divided into the following steps:

  • 1. Check whether key is null

  • 2. Find setXXX, _setXXX, setIsXXX

  • 3, determine whether response accessInstanceVariablesDirectly method, namely indirect access the instance variables,

    • returnYES, proceed to the next set value,
    • If it isNOCollapse,
  • 4. Assign values to indirect access variables (only once) in the following order: _key, _isKey, key, isKey

    • 4.1 Define a mutable array to collect instance variables
    • 4.2 throughclass_getInstanceVariableMethod to obtain the corresponding IVAR
    • 4.3 throughobject_setIvarMethod to set a value for the corresponding IVAR
  • 5. If the related instance variable cannot be found, throw an exception

/ / set value
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
    
// 1. Check whether key exists
    if (key == nil || key.length == 0) return;
    
// 2, find setXXX, _setXXX, setIsXXX
    // Use uppercase key
    NSString *Key = key.capitalizedString;
    // Use uppercase key
    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, determine whether response ` accessInstanceVariablesDirectly ` method, namely indirect access the instance variables, returns YES, set values continue to the next step, if NO, the collapse
    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. Assign values to indirect access variables in _key, _isKey, key, isKey order
    // 4.1 Defines a mutable array to collect instance variables
    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 Obtaining the ivAR
        Ivar ivar = class_getInstanceVariable([self class]._key.UTF8String);
        // 4.3 Set the ivAR value
        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;
    }
    
// if not found, throw an exception
    @throw [NSException exceptionWithName:@"lgUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
    
}
Copy the code

The KVC value is customized

The custom code for the value is as follows

  • 1. Check whether key is null

  • 2. Find methods in the order: Get

    ,

    , countOf

    , and objectIn

    AtIndex



  • 3, determine whether can direct assignment instance variables, which determine whether response accessInstanceVariablesDirectly method, indirect access the instance variables,

    • returnYESTo proceed to the next step
    • If it isNOCollapse,

  • _is< key> is< key>

    • 4.1 Define a collection instance variableAn array variable
    • 4.2 throughclass_getInstanceVariableMethod to obtain the corresponding IVAR
    • 4.3 throughobject_getIvarMethod to return the value of the corresponding IVar
/ / value
- (nullable id)lg_valueForKey:(NSString *)key{
    
// 1
    if (key == nil || key.length == 0) {
        return nil;
    }
    
Get 
       
       
         countOf
        
          objectIn
         
          AtIndex
         
        
       
      
    // Use uppercase key
    NSString *Key = key.capitalizedString;
    // Splice method
    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)];
    }
    // Set type
    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, determine whether response ` accessInstanceVariablesDirectly ` method, namely indirect access the instance variables, returns YES, set values continue to the next step, if NO, the collapse
    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];
    }
    
_is< key>, _is< key>, is< key>
    // 4.1 Defines a mutable array to collect instance variables
    NSMutableArray *mArray = [self getIvarListName];
    For example, _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 @"";
}
Copy the code

KVC usage scenarios

1. Dynamically set values and values

  • Common ones can passsetValue:forKey:valueForKey:
  • Also throughroutingThe way ofsetValue:forKeyPath:valueForKeyPath:

2. Access and modify private variables through KVC

In daily development, private attributes of a class are not directly accessible to externally defined objects, but as far as KVC is concerned, an object has no privacy of its own, so any private attributes can be modified and accessed through KVC

3, multi-value operation (model and dictionary interturn)

Model and dictionary transformations can be implemented using the following two KVC apis

// dictionary to model
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

// Model to dictionary
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
Copy the code

Modify some internal properties of system space

Metagtext @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText @metagText

5. Use KVC to achieve higher-order messaging

When using KVC on a container class, valueForKey: is passed to every object in the container. Instead of operating on the container itself, the result is added to the returned container, making it easy to operate on the collection to return another collection

As follows:

//KVC implements higher-order messaging
- (void)transmitMsg{
    NSArray *arrStr = @[@"english"The @"franch"The @"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); }}//******** The result is printed ********
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
Copy the code

reference

This article learns and references iOS- Basic Principle 22: KVC basic principle, thanks here

The article lists