Key-value Coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects use to access their properties indirectly. A property can be accessed through a string key. This indirect access mechanism complements the direct access provided by instance variables and their associated accessor methods.

KVC related API

Commonly used method

There are four commonly used methods

  • throughkeySet the value/value
- (nullable id)valueForKey:(NSString *) 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; // use KeyPath to set the value - (void)setValue:(nullable id)value forKeyPath:(NSString *) KeyPath;Copy the code

Other methods

/ / the default returns YES, said if not found the Set < Key > method, according to _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 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 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

KVC set value underlying principle

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

  • Directly throughsetterMethods the assignment
  • Associated API assignments encoded by KVC key values
LGPerson *person = [[LGPerson alloc] init]; Person. name = @" cjl_ha "; // 2, person setValue:@"CJL_ 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

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 you have any of these setter methods, set the value of the property directly.

    • If none, go to [Step 2]

  • 】 【 the second step: if you don’t have the first step of three simple setter method, is to find whether 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 through setValue:forKey: method to set the attribute name of LGPerson’s object person 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>


    • If found, go to [step 5]

    • If not, go to [Step 2]

  • 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 found, please continue to [step 3]

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

    , enumeratorOf

    , and memberOf

    methods are searched simultaneously


    • If all three methods are found, a collection proxy object that responds to all NSSet methods is created and returned. This proxy object then converts all NSSet messages it receives to countOf

      , enumeratorOf

      , and memberOf

      : 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 InstanceVariablesDirectly 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 is an object pointer, the result is returned directly

    • If it is a scalar type supported by NSNumber, store it in an NSNumber instance and return it

    • If it is a scalar type not supported by NSNumber, convert it to an NSValue object and return it

  • If all the methods in the previous five steps fail, the system will execute the setValue: forUndefinedKey: method of the object. By default, an exception of type NSUndefinedKeyException is thrown

To sum up, KVC uses valueForKey: to set the attribute name of LGPerson’s object person as an example, as shown in the figure below

Custom KVC

Principle: Implement custom cjl_setValue:forKey: and cjl_valueForKey: methods by adding classified CJLKVC to NSObject, according to the search rules provided in the Official Apple documentation

@interface NSObject (CJLKVC) // set the value - (void)cjl_setValue:(nullable id)value forKey:(NSString *)key; // value - (nullable id)cjl_valueForKey:(NSString *)key; @endCopy the code

Custom KVC setting

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

  • 1, judgment,The key is not empty
  • 2, findsetterMethods, the order is:SetXXX, _setXXX, setIsXXX
  • 3. Determine whether to respondaccessInstanceVariablesDirectlyMethod, which accesses instance variables indirectly,
    • returnYES, proceed to the next set value,
    • If it isNOCollapse,
  • 4, indirect access variable assignment (will only go once), the order is:_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 the value - (void) cjl_setValue: (nullable value forKey: (id) nsstrings *) key {/ / 1, determine the key whether there is an if (key = = nil | | key. The length = = 0) return; NSString * key = key.capitalizedString; // setXXX * key = key.capitalizedString; NSString *setKey = [NSString stringWithFormat:@"set%@:", key]; NSString *_setKey = [NSString stringWithFormat:@"_set%@:", Key]; NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:", Key]; if ([self cjl_performSelectorWithMethodName:setKey value:value]) { NSLog(@"*************%@*************", setKey); return; }else if([self cjl_performSelectorWithMethodName:_setKey value:value]){ NSLog(@"*************%@*************", _setKey); return; }else if([self cjl_performSelectorWithMethodName:setIsKey value:value]){ NSLog(@"*************%@*************", setIsKey); return; } / / 3, determine whether response ` accessInstanceVariablesDirectly ` method, namely indirect access the instance variables, returns YES, continue to the next set value, if it is NO 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]; NSMutableArray *mArray = [self getIvarListName]; NSMutableArray = [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 = class_getInstanceVariable([self class], _key.UTF8String); Object_setIvar (self, ivar, value); // set 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; @throw [NSException exceptionWithName:@"CJLUnknownKeyException" 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,

    • Returns YES and proceeds to the next value

    • If it is NO, it crashes


  • _is< key> is< key>

    • 4.1 Define a mutable array to collect instance variables

    • 4.2 Obtain the ivAR by using the class_getInstanceVariable method

    • 4.3 Using the object_getIvar method, return the ivAR value

/ / value - (nullable id) cjl_valueForKey: (nsstrings *) key {/ / 1, the judgment is not empty the if (key = = nil | | key. The length = = 0) {return nil. } // get<Key> <Key> countOf<Key> objectIn<Key>AtIndex 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)];  } / / collection types else if ([self respondsToSelector: NSSelectorFromString (countOfKey)]) {if ([the 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, continue to the next set value, if it is NO 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 to instance variables in the following order: _<key>, _is< key>, <key>, is< key> // 4.1 Define 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 @""; return @""; }Copy the 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; // use KeyPath to set the value - (void)setValue:(nullable id)value forKeyPath:(NSString *) KeyPath;Copy the code

Refer to the following case

//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; Person setValue:@" hee hee "forKeyPath:@"student.name"]; NSLog(@"%@",[person valueForKeyPath:@"student.name"]); } return 0; } / / * * * * * * * * * * * * * printing result * * * * * * * * * * * * * 2020-10-27 09:55:08. 512833 + 0800 001 - KVC profile (58089-6301894) before the change: CJL 2020-10-27 09:55:08.512929+0800 001-KVC [58089:6301894Copy the code

KVC usage scenarios

1. Dynamically set values and values

  • SetValue :forKey: and valueForKey:

  • SetValue :forKeyPath: and 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 model - (void) setValuesForKeysWithDictionary: (NSDictionary keyedValues < > nsstrings *, id *); / / model dictionary - (NSDictionary < > nsstrings *, id *) dictionaryWithValuesForKeys: (NSArray keys < > nsstrings * *);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 shown below.

If (void)transmitMsg{NSArray *arrStr = @[@" English ", @"franch", @" Chinese "]; if (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); }} / / * * * * * * * * printing result * * * * * * * * 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 The 2020-10-27 11:33:43. 378417 + 0800 CJLCustom [60035-6380757] 7Copy the code