KVC is a common function in our daily development, commonly known as’ key value coding ‘, this chapter will explore how we commonly used KVC is implemented

What is the KVC

The basic definition

KVC stands for key-value Coding, which allows you to access and modify attributes through keys.

We can see its definition and usage by looking at Apple’s official documentation.

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.

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 the key-value encoding, its properties can be accessed via string arguments through a compact, uniform messaging interface. This indirect access mechanism complements the direct access provided by instance variables and their associated accessor methods.

Through the document, we know the following points:

  • KVC is an indirect access to object properties
  • Usually bygetandsetMethod to evaluate and set values
  • Is a basic programming idea

Basic usage

There are two basic uses: assignment and value. The main APIS are as follows

The assignment

// Assign - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
Copy the code

The values

// value - (id)valueForKeyPath:(NSString *)keyPath; - (id)valueForKey:(NSString *)key;Copy the code

Several ways to use KVC

First, set up a LGPerson class and explain it by assigning different types of attributes to the class. The code is as follows

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

@interface LGPerson : NSObject{
   @public
   NSString *myName;
}

@property (nonatomic, copy)   NSString          *name;
@property (nonatomic, strong) NSArray           *array;
@property (nonatomic, strong) NSMutableArray    *mArray;
@property (nonatomic, assign) int age;
@property (nonatomic)         ThreeFloats       threeFloats;
@property (nonatomic, strong) LGStudent         *student;

@end
Copy the code

Base object usage

Normal use

The use of basic objects is usually the most commonly used in development, such as the name attribute in the class, the myName member variable defined as public and the age attribute of the basic type int, directly use the basic usage of the above value can be assigned, the code is as follows

[person setValue:@"WY" forKey:@"name"];
[person setValue:@19 forKey:@"age"];
[person setValue:@"WYY" forKey:@"myName"];
NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- print result -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- WY - 19 - WYYCopy the code

Using the hierarchy

The hierarchy is also easier to understand, mainly using the setValue:forKeyPath: related method. For example, if you want to assign to the student attribute of the class, you cannot directly assign. Instead, you need to use point syntax for hierarchical assignment, as shown below

LGStudent *student = [[LGStudent alloc] init];
student.subject    = @"iOS";
person.student     = student;
[person setValue:@"ios" forKeyPath:@"student.subject"];
NSLog(@"% @",[person valueForKeyPath:@"student.subject"]); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- print result -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the iosCopy the code

Collection type use

The use of a collection type assigns values to the array type properties in the Person object. In the above example, since the attribute is an immutable group, we cannot directly assign to the attribute. In general, we need to introduce intermediate variables to carry out equivalent substitution. Relevant codes are as follows

person.array = @[@"1"The @"2"The @"3"]; Person.array [0] = @"100";
NSArray *array = [person valueForKey:@"array"]; Create a new array with the value of array array = @[@"100"The @"2"The @"3"];
[person setValue:array forKey:@"array"];
NSLog(@"% @",[person valueForKey:@"array"]); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- print result -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- (100, 2, 3)Copy the code

It can be found that if you use intermediate variables to operate, the steps are relatively tedious, we can use mutableArrayValueForKey method to achieve the purpose of simplification, the relevant code is as follows

NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"100";
NSLog(@"% @",[person valueForKey:@"array"]); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- print result -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- (100, 2, 3)Copy the code

Through the above two methods, we can see that it is more convenient and efficient to use KVC method to read and write the set of variable method operations. The main methods are as follows

  • MutableArrayValueForKey: and mutableArrayValueForKeyPath:

    The returned proxy object is represented as an NSMutableArray object

  • MutableSetValueForKey: and mutableSetValueForKeyPath:

    The returned proxy object is represented as an NSMutableSet object

  • mutableOrderedSetValueForKey: and mutableOrderedSetValueForKeyPath:

    The returned proxy object is represented as an NSMutableOrderedSet object

The use of the collection operator

This part may be used less in normal development, mainly in valueForKeyPath, such as sum, average and other efficient operations to use.

It is mainly divided into the following three categories:

  • Aggregate operator

    • @avg: Returns the average value of the specified properties of the operation object
    • @count: Returns the number of specified properties of the operation object
    • @max: Returns the maximum value of the specified property of the operation object
    • @min: Returns the minimum value of the specified property of the operation object
    • @sum: Returns the sum of the specified attribute values of the operation object
  • Array operator

    • @distinctUnionOfObjects: returns a collection of properties specified by the operation object — de-duplication
    • @unionOfObjects: Returns the collection of properties specified by the operation object
  • Nested operators

    • @distinctUnionOfArrays: Returns the set of properties specified by the operation object (nested set) — de-duplicated, returns NSArray
    • @unionOfArrays: Returns the collection of specified properties of the operation object (collection)
    • @distinctUnionOfSets: returns the set of specified attributes of the operation object (nested set) — de-duplicated, returns NSSet

Non-object attribute use

Using scalar properties

As shown in the figure below, when the attribute is a basic scalar attribute, it can be converted to the NSNumber of the object type through the relevant method of NSNumber for corresponding read and write operations

Use of structure

As shown in the figure, the types of common structures are as follows. They can be converted to object attributes of type NSValue before corresponding read and write operations are performed

Person

ThreeFloats floats = {1., 2., 3.};
NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslut = [person valueForKey:@"threeFloats"];
NSLog(@"% @",reslut);
    
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- print result -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- {length = 12, Bytes = 0 x0000803f0000004000004040} 1.000000-2.000000-3.000000Copy the code

As you can see, when we store, we encode the structure, and we generate the NSValue variable, and we store it. Then, the value of the corresponding structure is extracted from NSValue to complete the process of reading and writing.

Validate attribute usage

KVC support properties, and this feature is through validateValue: forKey: error: (or validateValue: forKeyPath: error:) method to implement. The default implementation of this validation method is to look for a corresponding implementation of the validate< key >:error: method in the object that received the validation message (or the last object in keyPath) based on the key. If not, the validation succeeds by default and returns YES.

Because the validate

:error: method receives values and error parameters by reference, it has one of three results:

  1. The validation method considers the value object valid and returns it if YES does not change the value or error.

  2. The validation method considers the value object invalid but chooses not to change it. In this case, the method returns the NO error reference and sets the error reference (if provided by the caller) to an object whose NSError indicates the cause of the failure.

  3. The validation method considers the value object invalid, but creates a new valid object as a replacement. In this case, the method returns YES and the error object remains unchanged. Before returning, the method changes the value reference to point to the new value object. When modified, the method always creates a new object, not modifies the old object, even if the value object is mutable.

The relevant code is as follows

Person* person = [[Person alloc] init];
NSError* error;
NSString* name = @"John";
if(! [person validateValue:&nameforKey:@"name" error:&error]) {
    NSLog(@"% @",error);
}
Copy the code

The underlying implementation principle of KVC (Search pattern)

Through official documentation, we can clearly know how KVC performs set and GET at the bottom level

SetValue: forKey: method

  1. Search in order to see if it existsset<Key>,_set<Key>,setIs<Key>Methods. If so, the call is made directly
  2. If there is no method in condition 1. Priority judgmentaccessInstanceVariablesDirectlyIs the methodYESThe default value is YES, which is used to determine whether assignments to member variables are allowed. If it isYES. The member variables are found in order_Key,_isKey,Key,isKey, if the corresponding member variable exists, the value is directly assigned; if not, the next step is 3.
  3. If none of the conditions in 1 and 2 are met, thenSetValue: forUndefinedKey:An error

The illustration below

GetValue: forKey: method

  1. Find methods in the order get

    ,

    , is

    , and _< Key>.


    • If found, attach the method return value and jump to step 5
    • If not, go to step 2
  2. Find if there are countOf

    and objectIn

    AtIndex: methods (corresponding to the original method defined by the NSArray class) and

    AtIndexes: Method (corresponding to the NSArray method objectsAtIndexes:)


    • If you find the first of these (countOf<Key>), and find at least one of the other two, create a proxy collection object that responds to all of the NSArray methods and return that object. (The translation is eithercountOf<Key> + objectIn<Key>AtIndex:, eithercountOf<Key> + <key>AtIndexes:, eithercountOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:)
    • If not, go to step 3
  3. Find three methods named countOf

    , enumeratorOf

    , and memberOf

    (corresponding to the original method defined by the NSSet class)


    • If all three methods are found, a response all is createdNSSetMethod, and returns the object
    • If not, go to Step 4
  4. Judge class methods accessInstanceVariablesDirectly results

    • If the returnYES, the_<key>._is<Key>.<key>.is<Key>Search for member variables in the order of, if found, add the member variable jump to step 5, if not, jump to step 6
    • If the returnNO, go to step 6
  5. Determine the value of the fetched attribute

    • If the property value is an object, return it directly
    • If the property value is not an object, but can be converted toNSNumberType, the attribute value is converted toNSNumberType returns
    • If the property value is not an object, it cannot be converted toNSNumberType, the attribute value is converted toNSValueType returns
  6. Call valueForUndefinedKey:. By default, this throws an exception, but subclasses of NSObject can provide key-specific behavior.

The diagram is as follows:

Custom KVC

After understanding the basic usage and underlying principles of KVC, we can implement a simple custom KVC based on its underlying principles.

First we create a class of NSObject and prefix it with custom method names to decouple and avoid collisions with system methods

Custom assignment

According to the above summary, the main idea is as follows:

  1. Judge not empty
  2. Find ways to do itset<Key>,_set<Key>setIs<Key>
  3. Determines whether instance variables can be assigned directly
  4. Find the relevant instance variable and assign it
  5. If no relevant instance is found, an exception is thrown

The relevant codes are as follows:

- (void)lg_setValue:(nullable id)value forKey:(NSString *) Key {✅// 1: not nullif (key == nil  || key.length == 0) return; ✅// 2: Find out howset<Key> _set<Key> setNSString *Key = key.capitalizedString; ✅ // Splicing method 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: checks if instance variables can be assigned directlyif(! [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]; NSMutableArray *mArray = [self getIvarListName];} ✅// 4. // _<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); ✅// 4.3 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; } ✅// 5: 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

Custom value

According to the above summary, the main idea is as follows:

  1. Judge not empty
  2. Find ways to do itget<Key>,<key>countOf<Key>,objectIn<Key>AtIndex
  3. Determines whether instance variables can be assigned directly
  4. Find the relevant instance variable and assign it

The relevant code is as follows:

- (Nullable ID)lg_valueForKey:(NSString *)key{✅// 1: check whether the nullable ID is not nullif (key == nil  || key.length == 0) {
        returnnil; } ✅// 2: Get <Key> <Key> countOf<Key> objectIn<Key>AtIndex ✅ ✅// Concatenation 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: Determines whether instance variables can be assigned directlyif(! [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]; NSMutableArray *mArray = [self getIvarListName];} ✅// 4. // _<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];
    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

Relevant 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;
}
Copy the code

Here is only a very simple implementation, a lot of multi-threading and other situations are not considered, you can refer to DIS_KVC_KVO implementation, to further deepen the impression