preface

In daily development, we often use KVC to assign values or access private properties. So what is KVC and how does it work? Let’s explore and analyze it.

KVC profile

  • Key-value coding(KEY-value coding)Is composed ofNSKeyValueCoding Informal protocolEnabled mechanism that an object uses to provide indirect access to its properties.When an object conforms to key-value encoding, its properties can be addressed with string arguments through a concise, unified messaging interface. This indirect access mechanism complements the direct access provided by instance variables and their associated access methods.
  • To viewsetValue:forKey:Source code, finally toFoundatinFramework of theNSKeyValueCodingFile:



However, Foundation is not open source, so you can only check out KVC’s official documentation: Key-Value Coding Programming Guide

Replication and value of KVC

First determine the location to see, according to the document foundAccessor Search Patterns:

  • First to seesettermethods

Basic Setter

  • The document says callsetValue:forKey:When you assign a value to a property,
      1. They’ll look in orderset<Key>or_set<Key>And if I find it, I assign it,
      1. If it’s not found, if the class methodaccessinstancevariablesdirectThe return value isYES, will look for instance variables in order_<key>._is<Key>.<key>Or,is<Key>, and assign it a value
      1. If I don’t find it, I’ll leavesetValue:forUndefinedKey:methods

Code validation

    1. Let’s define oneLGPersonClass and set instance variables to implementsetNameand_setNameMethod, finally inViewControllerIn the callsetValue:forKey:Methods:
// .h
@interface LGPerson : NSObject {
@public
    NSString *_isName;
    NSString *name;
    NSString *isName;
    NSString *_name;
}

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

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

// ViewController.m
LGPerson *person = [[LGPerson alloc] init];
[person setValue:@"wushuang" forKey:@"name"];
Copy the code

The print result is as follows:



The setValue:forKey: method is called, and the setName method is commented out, leaving _setName, and then run:



This time we go to the _setName method, which means that when we call setValue:forKey:, we look for the setName method first, and if we don’t find it, we look for _setName.

  • whensetNameand_setNameWhen they’re not implemented, they’re looking for instance variables_name._isName.nameOr,isName

If isName and _isName are added, then setIsName and _setIsName are added. Let’s verify:

  • Comment the first two firstsetMethod, and then addsetIsNameand_setIsNameMethods:
- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
Copy the code
  • Run the results found that gosetIsNameThe method did not go_setIsName, and you get the new process:

New process: after calling setValue:forKey: the lookup order is setName -> _setName -> setIsName

  • whensetIf the associated method is not found, it will look for the associated instance variable and comment it out firstsetRelated methods, and then implement the class methods firstaccessInstanceVariablesDirectly, the return value isYESTo print the value of the instance variable:
// LGPerson.m
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

// ViewController.m
NSLog(@ "% @ - % @ - % @ - % @",person->_name,person->_isName,person->name,person->isName);
Copy the code

Running result:



  • Will be find_name, comment out the first printed instance variables and then print them, and finally get the order in which the instance variables are printed:_name -> _isName -> name -> isName
  • Comment out the four instance variables and implement the methodsetValue:forUndefinedKeyThe print:
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"%s -- %@ --- %@", __func__, value, key);
}
Copy the code

Here are the results:



  • Shown that whenSet methodandThe instance variablesWhen they don’t, they leavesetValue:forUndefinedKeyMethods.

The flow chart

  • Basic SetterThe process is as follows:

Basic Getter

Search Pattern for the Basic Getter:



  • Which is to implementvalueForKey:Method, the system takes the following steps (regardless of the collection type) :
      1. Search the instance method for the first namegetName,name,isNameor_nameThe accessor method of. If it is found, it is called
      1. If it is not found (remove the collection type), check the class method firstaccessInstanceVariablesDirectlyImplementation and returnYESAnd then search for instance variables in turn_name._isName.nameOr,isNameGets the value of the instance variable and returns it
      1. If I don’t find it, I’ll leavevalueForUndefinedKey:methods

Code validation

After commenting all the code associated with the set, implement the first step:

// LGPerson.m
- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

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

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

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

// ViewController.m
NSLog(@" Value: %@",[person valueForKey:@"name"]);
Copy the code
  • Then, after printing one, comment out the printed one and run it to get the order of value of the instance methods in step 1:getName -> name -> isName -> _name

Then comment out the instance method and assign the instance variable:

// ViewController.m
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";

NSLog(@" Value: %@",[person valueForKey:@"name"]);
Copy the code
  • And then I print out one,annotationDrop aAssignment to instance variablesandThe instance variables, and then print to get the value order of the instance variables in the second step:_name -> _isName -> name -> isName
  • Comment out the instance variable and its assignment, and thenLGPerson.mimplementationvalueForUndefinedKey:Methods:
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"%s --- %@", __func__, key);
    return NSStringFromSelector(_cmd);
}

// ViewController.m
NSLog(@" Value: %@",[person valueForKey:@"name"]);
Copy the code
  • The running results are as follows:



So far, the three steps of the value process can be verified

The flow chart

  • Basic SetterThe search process is as follows:

Custom KVC

  • To understand theKVCthesetterandgetterAnd then we can define it ourselvesKVC? The answer is yes.
  • According to theNSKeyValueCodingIn thesetterandgetterMethod, in the definition ofNSOjbectCategory defines newsetterandgetterMethod, and then defineWSPersonAnd there aresetInstance methods and instance variables:
@interface NSObject (WSKVC)

- (void)ws_setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)ws_valueForKey:(NSString *)key;
@end

// WSPerson.h
@interface WSPerson : NSObject {
@public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end

// WSPerson.m
@implementation WSPerson
+ (BOOL)accessInstanceVariablesDirectly {
    return true;
}
- (void)setName:(NSString *)name {
    NSLog(@"%s --- %@", __func__ ,name);
}
- (void)_setName:(NSString *)name {
    NSLog(@"%s --- %@", __func__ ,name);
}
- (void)setIsName:(NSString *)name {
    NSLog(@"%s --- %@", __func__ ,name);
}
@end
Copy the code
  • The custom methods are then processed according to the value and assignment process analyzed earlier

The assignmentws_setValue:forKey:

The code is as follows:

- (void)ws_setValue:(nullable id)value forKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        return;
    }
    NSString *Key = key.capitalizedString; // Capitalize the first letter
    // Concatenate the associated method names
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];

    // Determine whether to implement the three instance methods in sequence
    if ([self ws_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@ "___ % @ ___." ",setKey);
        return;
    } else if ([self ws_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@ "___ % @ ___." ",_setKey);
        return;
    } else if ([self ws_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@ "___ % @ ___." ",setIsKey);
        return;
    }

    if(! [self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"WS_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****".self] userInfo:nil];
    }

    // Get the names of all instance variables
    NSMutableArray *mArray = [self getIvarListName];
    // Concatenate the required instance variable name
    NSString *_key = [NSString stringWithFormat:@ "_ % @",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];

    // Determine whether the instance variable name is in the array of instance variables. If it exists, get the instance variable and assign a value to it
    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;
    } 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 an exception if the relevant instance variable cannot be found
    @throw [NSException exceptionWithName:@"WS_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****".self.NSStringFromSelector(_cmd)] userInfo:nil];
}

- (BOOL)ws_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
    // Determine whether the method can respond,
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        // Call if it can respond
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

// Get the name of the instance variable
- (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;
}
Copy the code
    1. To determinekeyIf there is a
    1. Then judge the relevant ones in ordersetMethods:setName: -> _setName -> setIsName
    • Concatenate the names of the three methods and call them in turnrespondsToSelectorMethod to determine whether it can respond, and if it can respond, useperformSelectorMethod execution call
    1. If the class methodaccessInstanceVariablesDirectlyThe return value isNO, an exception is thrown
    1. If the returnYESGets an array of instance variable names for the class, and then based on the names of the associated instance methodsAccording to the orderDetermine if it is in the array and get the instance variable if it isivarAnd then assign a value to it
    1. An exception is thrown when the instance method cannot find a value either

The valuesws_valueForKey:

The code is as follows:

- (nullable id)ws_valueForKey:(NSString *)key {
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // Capitalize the first letter
    NSString *Key = key.capitalizedString;
    // Concatenate the method name, which is partially the same as the instance variable name
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    NSString *_key = [NSString stringWithFormat:@ "_ % @",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(isKey)]) {
        return [self performSelector:NSSelectorFromString(isKey)];
    } else if ([self respondsToSelector:NSSelectorFromString(_key)]) {
        return [self performSelector:NSSelectorFromString(_key)];
    }
    // Set type processing
    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

    // Determine whether an instance variable can be assigned directly
    if(! [self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"WS_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****".self] userInfo:nil];
    }
    // Get the instance variable name
    NSMutableArray *mArray = [self getIvarListName];

    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    // Check whether the concatenated instance variable name is in the array of instance variables. If it exists, get the instance variable and take a value
    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);;
    }
    @throw [NSException exceptionWithName:@"WS_UnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****".self.NSStringFromSelector(_cmd)] userInfo:nil];
    return @ "";
}
Copy the code
    1. To determinekeyReturns if it exists, does not exist, or is nullnil
    1. Then judge the relevant ones in ordergetExample method:getName: -> name -> isName -> _name
    • Concatenate the names of the methods and call them in turnrespondsToSelectorMethod to determine whether it can respond, and if it can respond, useperformSelectorMethod execution call
    1. If the class methodaccessInstanceVariablesDirectlyThe return value isNO, an exception is thrown
    1. If the returnYESGets an array of instance variable names for the class, and then based on the names of the associated instance methodsAccording to the orderDetermine if it is in the array and get the instance variable if it isivarAnd then the value
    1. When the instance method cannot find the value either, it throws an exception and returns null