IOS underlying principles + reverse article summary

Key-value Coding is a mechanism enabled by the NSKeyValueCoding informal protocol, which is used by an object to indirectly access its attributes. You can access a property 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 mainly four commonly used methods

  • throughkeyLet’s set the value/the 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
// set the nullable id to KeyPath. - (nullable id)valueForKeyPath:(NSString *)key; - (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 property values. It can be used to check if a set value is correct, make a replacement value for an incorrect value, or reject setting a new value and return the reason for the error. - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; If the property is an NSMutableArray, then you can use this method to return the property. - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; // This method is called if the Key does not exist and KVC cannot find any fields or properties related to the Key. By default, an exception is thrown. - (nullable id)valueForUndefinedKey:(NSString *)key; // The same as the previous method, but this method sets the value. - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; // if you pass nil to Value during SetValue, this method will be called - (void)setNilValueForKey:(NSString *)key; // Enter a set of keys, return the Value corresponding to the set of keys, and 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 the underlying principle

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

  • Directly throughsetterMethods the assignment
  • Related API assignments encoded by KVC key values
LGPerson *person = [[LGPerson alloc] init]; Person. Name = @"CJL_ haha "; [person setValue:@"CJL_ hee "forKey:@"name"];Copy the code

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

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

  • throughHopperDisassembles to see the pseudocode
  • By appleThe official documentation
  • GithubSearch for relevant demo

Here, we study the key-value Coding Programming Guide in apple’s official documentation, which describes the setting process as follows

When setValue:forKey: is called to set the property value, the underlying execution process is

  • [Step 1] First of all, find out whether there are these three kindssetterMethod, in search orderSet <Key> : -> _set<Key> -> setIs<Key>
    • If there is one of these setter methods, set the value of the property directly (main note: key is the name of the member variable, first character case must conform to KVC naming convention).

    • If neither, go to step 2.

  • [Step 2] : If the three simple setter methods in step 1 are not available, look for themaccessInstanceVariablesDirectlyWhether to returnYES.
    • If the returnYES, then find the indirectly accessed instance variables for assignment, and the search order is:_<key> -> _is<Key> -> <key> -> is<Key>
      • If any of the instance variables are found, the value is assigned

      • If none, go to step 3.

    • If the returnNO, then enter [Step 3]
  • If neither the setter method nor the instance variable is found, the system executes the objectSetValue: forUndefinedKey:Method is thrown by defaultNSUndefinedKeyExceptionType of exception

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

Underlying principle of KVC value

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

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

  • [Step 1] First look up the getter method, according toget<Key> -> <key> -> is<Key> -> _<key>Method sequence lookup,
    • If found, go to [Step 5]

    • If not, go to step 2.

  • If the getter method in step 1 is not found, KVC will look for itCountOf <Key> and objectIn <Key> AtIndex: and <Key> AtIndexes:
    • If countOf

      and one of the other two are found, a collection proxy object is created that responds to all NSArray methods and returns that object, NSKeyValueArray, which is a subclass of NSArray. The proxy object then converts all the NSArray messages received into countOf

      , objectIn

      AtIndex: and

      AtIndexes: some combination of messages, which are used to create the key-value encoding object. If the original object also implements an optional method called get

      : range:, then the proxy object will also use that method where appropriate (note: The naming rules for method names should follow KVC’s standard naming methods, including method signatures).

    • If you do not find these three accessing arrays, please continue to [Step 3].

  • [Step 3] If none of the above methods are found, they will be searched simultaneouslyEnumeratorOf <Key> and memberOf<Key>These three methods
    • 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 object

    • If not, go to step 4.

  • [Step 4] If not found, check the class methodInstanceVariablesDirectlyWhether or notYES, search in turn_<key>, _is< key>, <key> or is< key>Instance variables of
    • If you find it,Get the value of the instance variable directlyEnter [Step 5]
  • [Step 5] According to the searchThe type of the property valueTo return different results
    • 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 that NSNumber does not support, convert it to an NSValue object and return that object

  • [Step 6] If the above 5 steps methodAll failure, the system will execute the objectvalueForUndefinedKey:Method is thrown by defaultNSUndefinedKeyExceptionType of exception

To sum up,KVC uses the valueForKey: method to set the valueThe process to setLGPersonOf the object personnameAs shown in the following figure

Custom KVC

How it works: by adding the classification CJLKVC to NSObject, you can implement custom cjl_setValue:forKey: and cjl_valueForKey: methods according to the search rules provided in the official apple documentation

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

Custom KVC Settings

The process of custom KVC setting is mainly divided into the following steps:

  • 1, judgment,The key is not empty
  • 2, findsetterMethods, in order:SetXXX, _setXXX, setIsXXX
  • 3. Determine whether to respondaccessInstanceVariablesDirectlyMethod, which accesses instance variables indirectly,
    • returnYES, continue to set the next value,
    • If it isNOCollapse,
  • 4, indirect access to variable assignment (only one walk), 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 the ivAR value
  • 5. Throw an exception if the relevant instance variable cannot be found
/ / 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; NSString * key = key.capitalizedString; NSString *setKey = [NSString stringWithFormat:@"set%@:", key]; 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 *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]) {// get ivar ivar ivar = class_getInstanceVariable(self class), _key.UTF8String); // 4.3 Setting 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; } // 5, @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

Customize the KVC value

The code for defining the value is as follows

  • 1. Check that the key is not empty

  • 2. Find the corresponding methods in the order of get

    ,

    , countOf

    , objectIn

    AtIndex

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

    • Return YES to continue the next step

    • If it is NO, it crashes

  • _

    _is< key>

    is< key>

    • 4.1 Define a mutable array to collect instance variables

    • 4.2 Obtain the corresponding IVAR using the class_getInstanceVariable method

    • 4.3 Return the ivAR value using the object_getIvar method

/ / 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 // Key to uppercase NSString *Key = key.capitalizedString; NSString *getKey = [NSString stringWithFormat:@"get%@",Key]; 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. Find the related instance variables and assign them in order: NSMutableArray *mArray = [self getIvarListName]; _is< key>, _is< key>, <key>, is< key>; // 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 routing access, or 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:

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

Consider the following example

//CJLPerson class @interface CJLPerson: NSObject @property (nonatomic, copy) NSString *age; @property (nonatomic, strong) CJLStudent *student; @end //CJLStudent @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 "]; person setValue:@" hee hee" forKeyPath:@"student. 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 introduction [58089:6301894Copy the code

KVC usage scenarios

1. Dynamic setting and value

  • The common ones are setValue:forKey: and valueForKey:

  • You can also setValue:forKeyPath: and valueForKeyPath by routing.

2. Access and modify private variables through KVC

In daily development, for the private property of the class, the object defined outside, is not directly access to the private property, but for KVC, an object does not have its own privacy, so you can modify and access any private property through KVC

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

The transformation between model and dictionary 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

4. Modify some internal properties of the system space

In daily development, we know that a lot of UI controls are by multiple UI in its internal space combination and become, these internal control apple does not provide access to the API, but use KVC can solve this problem, commonly used is the placeHolderText UITextField at custom tabbar and personalization

5. Realize high-level message passing with KVC

When using KVC on a container class, valueForKey: is passed to each object in the container instead of operating on the container itself, and the result is added to the returned container, making it easy to operate on a set to return another set

As shown below.

(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[60033:6380757] 2020-10-27 11:33:43.377860+0800 CJLCustom[60033:6380757] Chinese 2020-10-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7 2020-10-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 6 The 2020-10-27 11:33:43. 378417 + 0800 CJLCustom [60035-6380757] 7Copy the code

The appendix

CustomKVC complete code see Github-CustomKVC_KVO, like can point ❤