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 by
get
andset
Method 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:
andmutableOrderedSetValueForKeyPath:
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:
-
The validation method considers the value object valid and returns it if YES does not change the value or error.
-
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.
-
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
- Search in order to see if it exists
set<Key>
,_set<Key>
,setIs<Key>
Methods. If so, the call is made directly - If there is no method in condition 1. Priority judgment
accessInstanceVariablesDirectly
Is the methodYES
The 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. - If none of the conditions in 1 and 2 are met, then
SetValue: forUndefinedKey:
An error
The illustration below
GetValue: forKey: method
-
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
-
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
- If you find the first of these (
-
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 created
NSSet
Method, and returns the object - If not, go to Step 4
- If all three methods are found, a response all is created
-
Judge class methods accessInstanceVariablesDirectly results
- If the return
YES
, 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 return
NO
, go to step 6
- If the return
-
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 to
NSNumber
Type, the attribute value is converted toNSNumber
Type returns - If the property value is not an object, it cannot be converted to
NSNumber
Type, the attribute value is converted toNSValue
Type returns
-
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:
- Judge not empty
- Find ways to do it
set<Key>
,_set<Key>
、setIs<Key>
- Determines whether instance variables can be assigned directly
- Find the relevant instance variable and assign it
- 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:
- Judge not empty
- Find ways to do it
get<Key>
,<key>
、countOf<Key>
,objectIn<Key>AtIndex
- Determines whether instance variables can be assigned directly
- 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