preface

In iOS interviews, KVC is almost one of the must-ask questions. So what is KVC and how does it work?

KVC, also known as key-value Coding, is a common technology in iOS development. It is believed that many developers have used KVC. The two main methods are as follows:

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;

Copy the code

1. Introduction of KVC

It is often used in our daily development, as follows:

We can access object properties directly through setter/getter methods, or directly through member variables.
JPStudent *stu = [[JPStudent alloc] init];
stu.name      = @"RENO"; // setter
NSString *name   = stu.name; // getter
person->nickName = @ "rhino"; // Access the member directly

// KVC indirect access
[stu setValue:@kakashi forKey:@"name"];
NSString *name2 = [stu valueForKey:@"name"];
Copy the code

In addition to pure code development, KVC is often used in Xib and storyboard, as shown below:

The way you use it in Xib and storyboard is similar to the code, you set the key Value, the key is KeyPath, the Type of the Value, and then you set the Value Value.

1.1 KCV official documentation

  • Key-Value Coding Programming Guide

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.

  • NSKeyValueCoding

A mechanism by which an object’s properties can be accessed indirectly by name or key. The basic way to access an object’s value is that it sets the value of the property identified by the specified key, and it returns the value of the property identified by the specified key. Therefore, all properties of an object can be accessed in a consistent manner. setValue(_:forKey:)value(forKey:)

The default implementation relies on accessor methods that are typically implemented by objects (or directly access instance variables if needed)

1.2 KVC is commonly used

The two most basic APIS in KVC are setValue: forKey: and valueForKey:, which set and extract values according to the Key respectively, as shown in the example above. In addition, there are other ways to use it.

KVC – Collection type

Person. array[0] = @”100″

person.array = @[@ "1".@ "2".@ "3"];
Copy the code
  • Make a new array – KVC assignment OK

    // Create a new array - KVC assignment OK
    NSArray *array = [person valueForKey:@"array"];
    array = @[@ "100".@ "2".@ "3"];
    [person setValue:array forKey:@"array"];
    NSLog(@ "% @",[person valueForKey:@"array"]);
Copy the code

  • Second usemutableArrayValueForKey
/ / the second
    NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
    mArray[0] = @ "200";
    NSLog(@ "% @",[person valueForKey:@"array"]);
Copy the code

  • mutableArrayValueForKey
/* Given a key that identifies an _ordered_ to-many relationship, return a mutable array that provides read-write access to the related objects. Objects added to the mutable array will become related to the receiver, and objects removed from the mutable array will become unrelated. */
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

Copy the code

If you call this method when an object contains a data variable, you get a mutable array. By modifying the elements in the array, you change the original array, even if it is an immutable array.

The array @[@”1″, @”2″, @”3″] is an immutable array, which cannot be modified. However, by calling this method, we get a mutable array, and modify its values.

When we’re using an NSArray array, the most common thing we use is objectAtIndex, which is evaluated by subscript. NSArray and NSMutableArray also have valueForKey methods, as shown in the following code:

支那@interface支那NSArray<ObjectType> (NSKeyValueCoding)
/* Return an array containing the results of invoking -valueForKey: on each of the receiver's elements. The returned array will contain NSNull elements for each instance of -valueForKey: returning nil.*/- (* *id**)valueForKey:(NSString *)key;

/* Invoke -setValue:forKey: on each of the receiver's elements.*/- (* *void**)setValue:(**nullable* * * *id**)value forKey:(NSString *)key;
@end

Copy the code

Structural type

typedef struct {
    float x, y, z;
} ThreeFloats;

ThreeFloats floats = {1..2..3.};
    NSValue *value     = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSValue *value1    = [person valueForKey:@"threeFloats"];

Copy the code

Aggregate operator

#pragmaMark - Aggregate operator
@avg, @count, @max, @min, @sum
- (void)aggregationOperator{
    NSMutableArray *personArray = [NSMutableArray array];
    for (int i = 0; i < 6; i++) {
        JPStudent *p = [JPStudent new];
        NSDictionary* dict = @{
                               @"name":@"Tom".@"age": @ (18+i),
                               @"nick":@"Cat".@"length": @ (175 + 2*arc4random_uniform(6)),}; [p setValuesForKeysWithDictionary:dict]; [personArray addObject:p]; }NSLog(@ "% @", [personArray valueForKey:@"length"]);
    
    /// Average height
    float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
    NSLog(@"%f", avg);
    
    int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
    NSLog(@"%d", count);
    
    int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
    NSLog(@"%d", sum);
    
    int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
    NSLog(@"%d", max);
    
    int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
    NSLog(@"%d", min);
}

Copy the code

A dictionary type

#pragmaMark - Dictionary operation

- (void)dictionaryTest{
    
    NSDictionary* dict = @{
                           @"name":@"Cooci".@"nick":@"KC".@"subject":@"iOS".@"age": @18.@"length": @180
                           };
    JPStudent *p = [[JPStudent alloc] init];
    // dictionary to model
    [p setValuesForKeysWithDictionary:dict];
    NSLog(@ "% @",p);
    // Key array to model to dictionary
    NSArray *array = @[@"name".@"age"];
    NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
    NSLog(@ "% @",dic);
}
Copy the code

2. KVC set setValue:forKey

What is the KVC set? The description in the official document is as follows:

The general meaning is:

  • The default implementation of setValue:forKey: given the key and value parameters as input, the value of the property key is set to value, and the object is accepted inline, using the following steps:

  • Look in order for one with the name set

    : or the first accessor with the name _set

    , and if found, call it with the input value (or expand as needed) and finish.

  • If you don’t find a simple accessor, accessInstanceVariablesDirectly returns YES if the class methods, will continue to look for the name similar to _ < key >, _is < key >, < the key >, or is < key > an instance variable, to find in this order, if found, Set the variable directly with the input value (or unpack value) and finish.

  • Can’t find the accessor or instance variables, called setValue: forUndefinedKey: by default, this will throw an exception, but a subclass of NSObject may provide specific to the behavior of the key.

Taking [stu setValue:@”RENO” forKey:@”name”] as an 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

Everyone but oneself through code verification, here no longer code demonstrates, there needs to be key accessInstanceVariablesDirectly said

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.

So let’s try it out in code.

  • JPPerson.h
NS_ASSUME_NONNULL_BEGIN

@interface JPPerson : NSObject{
    @public
        NSString *_isName;
       NSString *name;
      NSString *isName;
    NSString *_name;

}
@end

NS_ASSUME_NONNULL_END
Copy the code
  • JPPerson.m
#pragmaMark - Turns off or on instance variable assignment
+ (BOOL)accessInstanceVariablesDirectly{
    return NO; } - (id)valueForUndefinedKey:(NSString *)key{
	 NSLog(@" Abnormal message printed, the key does not exist %@",key);
	 return nil; } - (void)setValue:(id)value forUndefinedKey:(NSString *)key{
	  NSLog(The key does not exist %@,key);
}

//MARK: -setkey
//- (void)setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
/ /}

//- (void)_setName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
/ /}

//- (void)setIsName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
/ /}

// No call
//- (void)_setIsName:(NSString *)name{
// NSLog(@"%s - %@",__func__,name);
/ /}
Copy the code
  • main.mcall
	LGPerson* person = [LGPerson new];
	[person setValue:@"NewName" forKey:@"name"];
	NSString* name = [person valueForKey:@"name"];
	NSLog(@"value for key : %@",name);

	NSLog(@"_name:%@",person->_name);
	NSLog(@"_isName:%@",person->_isName);
	NSLog(@"name:%@",person->name);
	NSLog(@"isName:%@",person->isName);
Copy the code
  • The following information is displayed on the console:
2021- 08. 13:08:46.060908+0800 002-kvc value & Assignment process [9684:199214] The key does not contain name2021- 08. 13:08:46.061885+0800 002-kvc value & Assignment process [9684:199214] The key does not have a name2021- 08. 13:08:46.062117+0800 002-kvc value & Assignment process [9684:199214] value for key : (null)
2021- 08. 13:08:46.062344+0800 002-kvc value & Assignment process [9684:199214] _name:(null)
2021- 08. 13:08:46.062480+0800 002-kvc value & Assignment process [9684:199214] _isName:(null)
2021- 08. 13:08:46.062597+0800 002-kvc value & Assignment process [9684:199214] name:(null)
2021- 08. 13:08:46.062699+0800 002-kvc value & Assignment process [9684:199214] isName:(null)
2021- 08. 13:08:46.391636+0800 002-kvc value & Assignment process [9684:199214] The key does not have a name2021- 08. 13:08:46.391807+0800 002-kvc value & Assignment process [9684:199214] : (null) valuesCopy the code

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: Methods.

3. The KVC value is valueForKey

When valueForKey: code is called, what is the underlying execution mechanism of OC? The official documentation is as follows:

The above translation can probably summarize several processes:

  • 1. Search for method implementations in the order of get

    ,

    , is

    , and _< Key>. If found, call the method and perform step 5.


  • 2. If it is not found in the previous step and is of an array type, call the array-related method.

  • 3. If the collection type is not found in the previous step, call the collection related method.

  • 4. If the above is not found, the judge whether accessInstanceVariablesDirectly to YES, if it is YES, then find the member variables in turn _ < key >, _is < key >, < the key >, is < key >, if found in step 5, otherwise in step 6

  • 5. If a value is found, process the value

    • If the retrieved value is an object pointer, the object is returned directly,
    • If it’s aNSNumberScalar, stores it inNSNumberAnd return,
    • If it’s not aNSNumberScalar, then stored inNSValueAnd in return
  • 6. If information 1 to 4 is not found, throw an exception

The KVC of the dictionary actually calls its own setObject:forKey: and objectForKey: methods, and does not look up its own member variables and attributes.

  • Value test code
//MARK: -valueForKey -get 
      
       , 
       
        , is
        
         , or _< Key>,
        
       
      

- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

//- (NSString *)name{
// return NSStringFromSelector(_cmd);
/ /}
//
//- (NSString *)isName{
// return NSStringFromSelector(_cmd);
/ /}
//
//- (NSString *)_name{
// return NSStringFromSelector(_cmd);
/ /}

Copy the code
  • The test code is as follows:
    person->_name = @"_name";
    person->_isName = @"_isName";
    person->name = @"name";
    person->isName = @"isName";

    NSLog(@ values: % @ "",[person valueForKey:@"name"]);
Copy the code

The above code is annotated and tested for print as follows

  • getName

  • name

I will not a test print screenshots, the other are the same, interested can go to the test!

Through print tests, the order of finding methods is exactly the same as the first step get

,

, is

, _< Key>, and no other methods are called.


  • Verify the lookup of a variable’s value by first commenting the method

  • And look at the print

ValueForUndefinedKey = ‘key’; valueForUndefinedKey = ‘key’;

  • So comment out an assignment to a member variable and see

If the value of _isName is null and the definition of _name is not annotated, the value of _isName is null.

This is because it is in order, although the _name assignment annotation, but its definition is still in, that is, the eldest is still in, the second can not be in, unless the eldest is not.

As shown below:

4. Customize KVC

We can also implement an informal protocol object ourselves using this mechanism to provide indirect access to its properties, custom KVC, as follows:

// KVC custom
@implementation NSObject (LGKVC)

/ / set
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
    // 1: Determine what key
    if (key == nil || key.length == 0) {
        return;
    }

    // 2: setter set<Key>: or _set<Key>,
    // Use uppercase key
    NSString *Key = key.capitalizedString;

    // Splice method
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];

    // Whether there is a method
    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 returns YES NO collapses
    // 3: check whether the instance variable -- NO can be assigned directly
    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: indirect variable
    // Get ivar -> iterate over containsObjct -
    // 4.1 Defines a mutable array to collect instance variables
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // Concatenate member variables
    NSString *_key = [NSString stringWithFormat:@ "_ % @",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];

    // Whether the corresponding variable exists
    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;
    }

    // 5: If no related instance is found

    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****".self.NSStringFromSelector(_cmd)] userInfo:nil];
}

/ / value
- (nullable id)lg_valueForKey:(NSString *)key{

    // 1: swipe key to check whether the key is not empty
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2: Find the related method 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)];
    }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];
            }
            returnmArray; }}#pragma clang diagnostic pop


    // 3: check whether the instance variables can be directly assigned -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. Find the relevant instance variable to assign
    // 4.1 Defines a mutable array to collect instance variables
    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];
    
    // Check whether the corresponding member variable exists
    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 @ "";
}

#pragmaMark **- Related methods **

- (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

5. To summarize

  • KVCThe full nameKey-Value Coding, commonly known as key-value encoding, provides a mechanism for indirect access to attributes that can be accessed through akeyThe string accesses a property
  • When I set the values and the values, ifkeyWrite wrong, compile without error, run time crash.
  • The associated is called first when the value is setsetMethod, and then inaccessInstanceVariablesDirectlyforYESIn this case, the corresponding member variable is assigned.
  • When the value is specified, correlation is also called firstgetMethod, if not, inaccessInstanceVariablesDirectlyforYESIn this case, the values of the relevant variables are taken.
  • rewrite+ (BOOL)accessInstanceVariablesDirectlyMethod to return itNOIn this case, ifKVCCould not findset<Key>,_set<Key>,setIs<Key>When related methods are used, they are directly usedSetValue: forUndefinedKey:Methods.
  • SetValue the process for setting a value is as follows: The process for setting a value is similar and I won’t draw it here