This article belongs to “Jane Book — Liu Xiaozhuang” original, please note:

< Jane books – Liu Xiaozhuang > https://www.jianshu.com/p/1d39bc610a5b


KVC is often used in work, but many people are not very clear about how it works. For example, how KVC searches and assigns values when it accesses.

There are many articles on KVC online, but many of them are not of high quality. Take time to write out my understanding of KVC in these two days, as a learning exchange, just let you help me correct it, thank you very much!


Protocol definition

KVC, which stands for KeyValueCoding, is an informal protocol defined in nsKeyValuecoding. h. KVC provides a mechanism for indirectly accessing its attribute methods or member variables through strings.

NSKeyValueCoding provides KVC common access methods, namely getter method valueForKey: and setter method setValue:forKey:, and its derived keyPath method, which are common to each class. KVC provides the default implementation, and we can rewrite the corresponding methods to change the implementation ourselves.

Basic operation

KVC operates on three types: basic data types, constants, object types, and collection types.

@interface BankAccount : NSObject
@property (nonatomic.strong) NSNumber *currentBalance;
@property (nonatomic.strong) Person *owner;
@property (nonatomic.strong) NSArray<Transaction *> *transactions;
@end
Copy the code

When using KVC, you can assign a value to an attribute by taking the attribute name as the key and setting value.

[myAccount setValue:@(100.0) forKey:@"currentBalance"];
Copy the code

keyPath

** In addition to assigning to the attributes of the current object, it is possible to assign to “deeper” objects. ** For example, assign the street property of the current object’s address property. KVC performs multi-level access directly using point syntax similar to attribute calls.

[myAccount setValue:@" Zhongguancun Street" forKeyPath:@"address.street"];
Copy the code

When you value an array using keyPath and store objects of the same type, you can use the valueForKeyPath: method to specify a field for all the objects in the array. For example, in the following example, valueForKeyPath: takes the name property values of all objects in the array and returns them in an array.

NSArray *names = [array valueForKeyPath:@"name"];
Copy the code

Multivalued operation

It is important to note that although you see the word ‘dictionary’, the following two methods are not dictionary methods.

KVC also has more powerful functions. It can obtain a set of values based on a given set of keys and return them in the form of a dictionary. After obtaining the dictionary, it can obtain a value from the dictionary through the key.

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
Copy the code

Similarly, batch assignments can also be done through KVC. In object call setValuesForKeysWithDictionary: method, can pass in a contains the key, the value of the dictionary in KVC all data can be carried out in accordance with the property names and dictionary of key match, and to give a value to the attribute of the User object assignment.

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
Copy the code

Practical skills

It is common to see dictionary-to-model assignments in projects, and if you assign values one by one in a custom init method, you will need to change the assignment statement every time the data changes. However, with the assignment API provided by KVC, it is possible to batch assign data. Suppose you have the following JSON data and define the User class, in the outside world through setValuesForKeysWithDictionary: method for the User assignment.

JSON data: {"username": "lxz"."age": 25."id": 100
}

@interface User : NSObject
@property (nonatomic.copy) NSString *name;
@property (nonatomic.assign) NSString age;
@property (nonatomic.assign) NSInteger userId;
@end

@implementation User
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"id"]) {
        self.userId = [value integerValue]; }}@end
Copy the code

Assignment will have some problems, for example, the server returns an id field, but for the client id system fields, keeping can override setValue: forUndefinedKey: methods and id parameter in internal treatment assignment.

The conversion requires that the server data match the class definition, as well as the number of fields and field names. If the User has more data than the server, the field not transmitted by the server is empty. If the data User passed by the server is not defined, it will cause a crash.

When KVC performs attribute assignment, the underlying data type is processed internally, and there is no need to manually convert NSNumber. Note that NSArray and NSDictionary collection objects, value can not be nil, otherwise it will Crash.

Exception information

If the corresponding key or keyPath cannot be found according to the KVC search rules, the corresponding exception method is invoked. The default implementation of the exception method throws an NSUndefinedKeyException when an exception occurs and the application crashes.

We can rewrite the following two methods to properly handle kVC-induced exceptions based on business requirements.

- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
Copy the code

Exception handling

When through the KVC to a non object attribute assignment is nil, the KVC invokes the object properties belong to setNilValueForKey: method, and throw NSInvalidArgumentException abnormalities, and makes the application Crash.

We can handle such exceptions when they occur by overriding the following method. For example, if you assign name to nil, you can override the setNilValueForKey: method and say name is null.

- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"name"{[])self setValue:@ ""ForKey: @ "age"]; }else{[supersetNilValueForKey:key]; }}Copy the code

Set property operation

According to the implementation principle of KVO, a new subclass is generated at run time and its setter methods are overridden, sending messages when its contents change. But this is only triggered by direct assignment to the property. If the property is a container object, adding or removing the container object will not call KVO’s methods. KVO can be used in conjunction with the CORRESPONDING API of KVC to trigger KVO when changes occur within the container object.

During the container object operation, the following methods are called to obtain the collection object through key or keyPath, and then add or remove the container object to trigger the KVO message notification.

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
Copy the code

KeyPath method:

- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
Copy the code

Set operator

The valueForKeyPath: method provided by KVC is a powerful way to “drill down” on collection objects by nesting collection operators in their keyPath, such as counting an attribute of an object in an array. (Collection objects refer mainly to NSArray and NSSet, but not NSDictionary)

The above expression is divided into three parts. The left part is the collection object to operate on. If the object calling KVC is a collection object, the left can be empty. The middle part is the expression, which usually begins with the @ sign. This is followed by the properties that perform the operation.

Set operators fall into three main categories:

  1. Collection operator: Handles the objects contained in the collection and returns different types depending on the operatorNSNumberGive priority to.
  2. Array operator: Returns the inclusion of qualified objects in an array, depending on the operator’s conditions.
  3. Nesting operator: Handles the nesting of other collection objects within a collection object and returns a collection object as a result.

example

Here is the test code created to facilitate simulating KVC operations. Transaction classes are defined as model classes that contain three types of properties. And define the BankAccount class, which contains an array, which the following code examples operate on, and which contains all Transaction objects.

@interface Transaction : NSObject
@property (nonatomic.strong) NSString *payee;
@property (nonatomic.strong) NSNumber *amount;
@property (nonatomic.strong) NSDate *date;
@end
Copy the code
@interface BankAccount : NSObject
@property (nonatomic.strong) NSArray *transactions;
@end
Copy the code

Set operator

The collection operator handles collection objects such as NSArray and NSSet and their subclasses, and returns objects of different types depending on the operator. The return value is usually NSNumber.

  • @avgTo compute the setright keyPathThe average value of the specified properties.
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
Copy the code
  • @countTo calculate the total number of sets.
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
Copy the code

Note: The @count operator is special in that it does not need to write the right keyPath and is ignored if it does.

  • @sumTo compute the setright keyPathThe sum of the specified properties.
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
Copy the code
  • @maxUsed to find the collectionright keyPathThe maximum value of the specified property.
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
Copy the code
  • @minUsed to find the collectionright keyPathThe minimum value of the specified property.
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
Copy the code

Note: @max and @min judge by calling compare: method, so we can rewrite this method to control the judgment process.

Array operator

  • @unionOfObjectsAll of the objects in the collectionpayeeObject is placed in an array and returned.
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
Copy the code
  • @distinctUnionOfObjectsAll of the objects in the collectionpayeeThe object is placed in an array and is returned after the array is deduplicated.
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
Copy the code

Note: In both of the above methods, if the property of the operation is nil, adding it to the array causes Crash.

Nested operators

Because the nested operators operate on nested collection objects, we create an arrayOfArrays object that contains two arrays that store objects of type Transaction.

NSArray*moreTransactions = .... ;NSArray *arrayOfArrays = @[self.transactions, moreTransactions];
Copy the code
  • @unionOfArraysIs used to manipulate the collection object inside the collection, and will allright keyPathThe corresponding object is returned in an array.
NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
Copy the code
  • @distinctUnionOfArraysIs used to manipulate the collection object inside the collection, and will allright keyPathThe corresponding objects are placed in an array and reweighted.
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
Copy the code
  • @distinctUnionOfSetsIs used to manipulate the collection object inside the collection, and will allright keyPathThe corresponding object is placed in oneset, and perform the discharge.
NSSet *collectedPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfSets.payee"];
Copy the code

tip

If the property being operated on in a collection object is of type NSNumber, you can simply use self to represent the value itself as follows.

NSArray *array = @[@(productA.price), @(productB.price), @(productC.price), @(productD.price)];
NSNumber *avg = [array valueForKeyPath:@"@avg.self"];
Copy the code

Non-object value processing

KVC supports basic data types and structures that can be converted to OC objects by NSValue and NSNumber in setters and getters. There is no such requirement in Swift, where all variables are objects.

The following is sample code for a structure conversion, which can be called initWithBool: the method wraps the underlying data type and can be implemented as a literal in addition to calling a method, such as the @(YES) call. The boolValue property of NSNumber is converted to the underlying data type.

@property (nonatomic.assign.readonly) BOOL boolValue;
- (NSNumber *)initWithBool:(BOOL)value NS_DESIGNATED_INITIALIZER;
Copy the code

The structure transformation code is defined in uigephysics. h, which exists in the Category of NSValue. NSValue provides conversion methods for CGPoint, CGRect and other structures. For example, the following is an example code for converting CGPoint.

@property(nonatomic.assign.readonly) CGPoint CGPointValue;
+ (NSValue *)valueWithCGPoint:(CGPoint)point;
Copy the code

It is important to note that no matter what time should give to nil in the setter, will lead to Crash and lead to abnormal NSInvalidArgumentException.

Property verification

The KVC can be authenticated by the following two methods, including key and keyPath. The validation method implementation returns YES by default, and the validation logic can be modified by overriding the corresponding method.

The validation method needs to be called manually and will not be called automatically during KVC.

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
Copy the code

The following is an example of using validation methods. In the internal implementation of the validateValue method, if there is a problem with the value or key passed in, the error can be indicated by returning NO and setting the NSError object.

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

Separate validation

KVC also supports validation of individual attributes by defining methods in the form validate

:error: and implementing validation code inside methods. When writing KVC validation code, you should first look for a property with a custom validate method, then look for the validateValue: method, and call your own method if it does, and return YES by default if neither method is implemented.

- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{
    if ((*ioValue == nil) | | ([(NSString *)*ioValue length] < 2)) {
        if(outError ! =NULL) {
            *outError = [NSError errorWithDomain:PersonErrorDomain
                                            code:PersonInvalidNameCode
                                        userInfo:@{ NSLocalizedDescriptionKey
                                                    : @"Name too short" }];
        }
        return NO;
    }
    return YES;
}
Copy the code

I think KVC should support automatic validation of validateValue, which is automatically validated when setValue or getValue is called. If the validation rules are not met, the call fails. This is cumbersome if the validateValue is called once for every place outside the world. Of course, there are also solutions, you can hook setValue and getValue methods through Method Swizzling Method.

Search rules

When KVC operates through key or keyPath, it can look up attribute methods, member variables, etc. Various names can be compatible during the search. The specific search rules should be based on official documents, so I translated the official documents and wrote them below.

inKVCThe implementation of the dependencysetterandgetterMethod implementation, so the method name should conform to the specification required by Apple, otherwise it will causeKVCFailure.

Before learning the search rules of KVC, it is important to understand the role of an attribute, which plays a very important role in the search process. This property indicates whether the value of the instance variable is allowed to be read, and if YES, the value of the property instance variable is read from memory during a KVC lookup.

@property (class.readonly) BOOL accessInstanceVariablesDirectly;
Copy the code

Basic Getter search pattern

This is the default implementation of valueForKey:. Given a key as an input parameter, proceed with the following steps inside the class that receives the valueForKey: method call.

  1. Search for instances through getter methods, such as get

    ,

    , is

    , _< Key> concatenation schemes. In this order, if you find a method that matches, you call the corresponding method and jump to step 5 with the result. Otherwise, proceed to the next step.


  2. If no simple getter method is found, the methods countOf

    , objectIn

    AtIndex:, and

    AtIndexes: search for its matching pattern.


    If the first of these and one of the other two are found, a collection proxy object is created that responds to all of NSArray’s methods and returns the object. Otherwise, proceed to step 3.

    The proxy object then sends the countOf

    , objectIn

    AtIndex:, and

    AtIndexes: messages received by NSArray to callers that comply with the KVC rules.


    When the proxy object and the KVC caller work together through the above methods, they are allowed to behave like NSArray.

  3. If the NSArray simple access method is not found, or the NSArray access method group. CountOf

    , enumeratorOf

    , memberOf

    : name methods.


    If three methods are found, a collection proxy object is created that responds to all NSSet methods and returns. Otherwise, go to Step 4.

    This proxy object then converts countOf

    , enumeratorOf

    , memberOf

    : method calls to the object on which it was created. In fact, this proxy object works with an NSSet to make it appear to be an NSSet.


  4. If not found simple getters, group or set access methods, and the return of the receiving accessInstanceVariablesDirectly class method is YES. Search for an instance named _

    , _is< key>,

    , is< key>, in their order.

    If the corresponding instance is found, immediately get the value available for the instance and go to step 5; otherwise, go to step 6.

  5. If an object pointer is retrieved, the result is returned. If an underlying data type is retrieved, but the underlying data type is supported by NSNumber, it is stored as NSNumber and returned. If an underlying data type is retrieved that does not support NSNumber, it is stored by NSValue and returned.

  6. If all fails, the valueForUndefinedKey: method is called and an exception is thrown, which is the default behavior. But subclasses can override this method.

Basic Setter search pattern

This is the default implementation of setValue:forKey:, given the input parameters value and key. Attempts to set the value of the property named key inside the receiving call object by doing the following:

  1. Look for setters named set

    : or _set

    , in that order, and if found, call the method and pass in the values (converting objects as needed).

  2. If have not found a simple setter, but accessInstanceVariablesDirectly class property returns YES, is to find a naming rules for _ < key >, _is < key >, < the key >, is < key > instance variables. In this order, value is assigned to the instance variable if found.

  3. If found no setter or instance variables, call the setValue: forUndefinedKey: method, and puts forward an exception by default, but a subclass of NSObject can recommend appropriate actions.

NSMutableArray Search mode

This is the default implementation of mutableArrayValueForKey:, which gives a key as an input parameter. In the object receiving the accessor call, return a mutable proxy array named key. This proxy array is the object used to respond to the external KVO.

  1. InsertObject :in

    AtIndex: and removeObjectFrom

    AtIndex:(equivalent to NSMutableArray’s original methods insertObject: AtIndex: and removeObjec TAtIndex: : or the method names are INSERT

    :atIndexes: and remove

    atIndexes: : : the same as insertObjects:atIndexes: and removeObject, the original methods of NSMutableArray SAtIndexes:).



    If at least one insert method and at least one remove method are found, a proxy object is returned, InsertObject :in

    AtIndex:, removeObjectFrom

    AtIndex:, insert

    :atIndexes:, And remove

    AtIndexes: messages.



    When an object receives a mutableArrayValueForKey: message and implements an optional replacement method, such as replaceObjectIn

    AtIndex:withObject: or replace

    AtIndexes: With

    :, Proxy objects use them when appropriate for best performance.


  2. If the object does not have a mutable array method, look for an alternative method named set

    :. In this case, a set

    : message is sent to the original responder of mutableArrayValueForKey: to return a proxy object in response to the NSMutableArray event.

    Tip: The mechanism described in this step is much less effective than the previous step, as it is possible to create new collection objects repeatedly rather than modify existing objects. Therefore, it should be avoided when designing your own KVC.

  3. If there is no variable array method, also found no accessors, but accept the response class accessInstanceVariablesDirectly property returns YES, is looking for a called _ < key > or < key > instance variables.

    In this order, a proxy object is returned if instance variables are found. The modified object will receive all messages sent by NSMutableArray, usually NSMutableArray or a subclass of it.

  4. If all cases fail, a mutable collection proxy object is returned. When it receives NSMutableArray news, sending a setValue: forUndefinedKey: message to receive mutableArrayValueForKey: the message of the original object.

    The setValue: forUndefinedKey: the default implementation is put forward a NSUndefinedKeyException is unusual, but subclasses can override this implementation.

Other search modes

There are also two search modes, NSMutableSet and NSMutableOrderedSet. These two search modes have the same steps as NSMutableArray, but the search and call methods are different. Detailed search methods can be found in the official KVC documentation, and then follow the above process to understand.

Code sample

According to the above description of the KVC lookup rules, we define a TestObject class, specify other setters and getters, and synthesize into other member variables to see if KVC can find the object of the attribute and assign the value.

@interface TestObject : NSObject {
    NSObject *_newObject;
}
@property (nonatomic.strong.setter=newSetObject:, getter=newObject) NSObject *object;
@property (nonatomic.strong) NSObject *twoObject;
@end

@implementation TestObject
@synthesize object = _newObject;
@end
Copy the code

Here, two properties are assigned, the twoObject property is assigned without any problems, and the second property causes Crash. The crash message throws an NSUnknownKeyException as described above and indicates that the object fetch method and instance object were not found.

TestObject *object = [[TestObject alloc] init];
[object setValue:[NSObject new] forKey:NSStringFromSelector(@selector(twoObject))];
[object setValue:[NSObject new] forKey:NSStringFromSelector(@selector(object))];
Copy the code

You can solve this problem by changing object to newObject to verify the KVC lookup rule above.

KVC performance

Based on the implementation principle of KVC above, we can see that KVC performance is not as fast as accessing attributes directly, although this performance cost is minimal. Therefore, when using KVC, it is recommended not to manually set the setter and getter of the property, which will lead to a longer search step.

And try not to use KVC for collection operations, such as NSArray, NSSet and so on, collection operations are more performance consumption, but also create unnecessary objects.

Private access

As we know from the above implementation, KVC is essentially a list of methods to manipulate and look up instance variables in memory. We can use this feature to access class private variables, such as the private member variables and attributes defined below in.m, which can be accessed via KVC.

This operation is accessible to readOnly properties and @protected member variables. If you don’t want to let the outside world access class member variables, can be accessInstanceVariablesDirectly attribute assignment to NO.

TestObject. M file@interface TestObject(a){
    NSObject *_objectOne;
}
@property (nonatomic.strong) NSObject *objectTwo;
@end
Copy the code

KVC also has many uses in practice, such as controls such as UITabbar or UIPageControl, which the system already packages for us, but does not provide enough API for style changes, which requires us to use KVC for operations.

You can customize a UITabbar object and then create the view you want internally and rearrange it internally using the layoutSubviews method. Then replace the Tabbar property of UITabbarController with a custom class via KVC.

Safety check

One problem with KVC is that because the key or keyPath passed in is a string, it is easy to write wrong or forget to modify the string after the property itself has been modified, causing crashes.

You can get around this problem by using the reflection mechanism in iOS, getting the SEL of the method via @selector(), and then reflecting the SEL as a string via NSStringFromSelector(). So when you pass a method name in @selector(), the compiler does a validation check, and raises a yellow warning if the method doesn’t exist or isn’t implemented.

[self valueForKey:NSStringFromSelector(@selector(object))];
Copy the code