Let’s start with an example
// -------------------- Cat -------------------- @interface Cat : NSObject @property (nonatomic, assign) NSInteger age; @end @implementation Cat @end // -------------------- ViewController -------------------- @interface ViewController () @property (nonatomic, strong) Cat *cat; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.cat = [[Cat alloc] init]; // Assignment [self.cat setValue:@2 forKey:@"age"]; / / value + output NSLog (@ "age = % @", [self. Cat valueForKey: @ "age"]); } @endCopy the code
Output:
2021-01-02 16:51:03.951534+0800 KVC[89501:2003082] age = 2
Copy the code
Well, the test worked, after all, it was just a simple KVC operation.
In actual development, KVC assignment operations have the following methods:
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
The value operations are as follows:
- (nullable id)valueForKey:(NSString *)key;
- (nullable id)valueForKeyPath:(NSString *)keyPath;
– (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; – (void)setValue:(nullable id)value forKey:(NSString *)key; Method is more powerful. It assigns values to properties of properties within an object. (Multiple properties can be joined)
For example, if the cat object has a fish property and the fish object has a count property, we can do this:
[self.cat setValue:@100 forKeyPath:@"fish.count"];
Copy the code
So how does a KVC assignment work? In fact, there are very detailed instructions in the Apple interface:
SetValue: forKey: method:
/* Given a value and a key that identifies an attribute, set the value of the attribute. Given an object and a key that identifies a to-one relationship, relate the object to the receiver, unrelating the previously related object if there was one. Given a collection object and a key that identifies a to-many relationship, relate the objects contained in the collection to the receiver, unrelating previously related objects if there were any. The default implementation of this method does the following: 1. Searches the class of the receiver for an accessor method whose name matches the pattern -set<Key>:. If such a method is found the type of its parameter is checked. If the parameter type is not an object pointer type but the value is nil -setNilValueForKey: is invoked. The default implementation of -setNilValueForKey: raises an NSInvalidArgumentException, but you can override it in your application. Otherwise, if the type of the method's parameter is an object pointer type the method is simply invoked with the value as the argument. If the type of the method's parameter is some other type the inverse of the NSNumber/NSValue conversion done by -valueForKey: is performed before the method is invoked. 2. Otherwise (no accessor method is found), if the receiver's class' +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _<key>, _is<Key>, <key>, or is<Key>, in that order. If such an instance variable is found and its type is an object pointer type the value is retained and the result is set in the instance variable, after the instance variable's old value is first released. If the instance variable's type is some other type its value is set after the same sort of conversion from NSNumber or NSValue as in step 1. 3. Otherwise (no accessor method or instance variable is found), invokes -setValue:forUndefinedKey:. The default implementation of -setValue:forUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application. Compatibility notes: - For backward binary compatibility with -takeValue:forKey:'s behavior, a method whose name matches the pattern -_set<Key>: It is also recognized in step 1. KVC accessor methods whose names start with assumptions were deprecated as of Mac OS 10.3 though. - For backward binary compatibility, -unableToSetNilForKey: will be invoked instead of -setNilValueForKey: in step 1, if the implementation of -unableToSetNilForKey: in the receiver's class is not NSObject's. - The behavior described in step 2 is different from -takeValue:forKey:'s, in which the instance variable search order is <key>, _<key>. - For backward binary compatibility with -takeValue:forKey:'s behavior, -handleTakeValue:forUnboundKey: will be invoked instead of -setValue:forUndefinedKey: in step 3, if the implementation of -handleTakeValue:forUnboundKey: in the receiver's class is not NSObject's. */ - (void)setValue:(nullable id)value forKey:(NSString *)key;Copy the code
ValueForKey: methods:
/* Given a key that identifies an attribute or to-one relationship, return the attribute value or the related object. Given a key that identifies a to-many relationship, return an immutable array or an immutable set that contains all of the related objects.
The default implementation of this method does the following:
1. Searches the class of the receiver for an accessor method whose name matches the pattern -get<Key>, -<key>, or -is<Key>, in that order. If such a method is found it is invoked. If the type of the method's result is an object pointer type the result is simply returned. If the type of the result is one of the scalar types supported by NSNumber conversion is done and an NSNumber is returned. Otherwise, conversion is done and an NSValue is returned (new in Mac OS 10.5: results of arbitrary type are converted to NSValues, not just NSPoint, NRange, NSRect, and NSSize).
2 (introduced in Mac OS 10.7). Otherwise (no simple accessor method is found), searches the class of the receiver for methods whose names match the patterns -countOf<Key> and -indexIn<Key>OfObject: and -objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSOrderedSet class) and also -<key>AtIndexes: (corresponding to -[NSOrderedSet objectsAtIndexes:]). If a count method and an indexOf method and at least one of the other two possible methods are found, a collection proxy object that responds to all NSOrderedSet methods is returned. Each NSOrderedSet message sent to the collection proxy object will result in some combination of -countOf<Key>, -indexIn<Key>OfObject:, -objectIn<Key>AtIndex:, and -<key>AtIndexes: messages being sent to the original receiver of -valueForKey:. If the class of the receiver also implements an optional method whose name matches the pattern -get<Key>:range: that method will be used when appropriate for best performance.
3. Otherwise (no simple accessor method or set of ordered set access methods is found), searches the class of the receiver for methods whose names match the patterns -countOf<Key> and -objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) and (introduced in Mac OS 10.4) also -<key>AtIndexes: (corresponding to -[NSArray objectsAtIndexes:]). If a count method and at least one of the other two possible methods are found, a collection proxy object that responds to all NSArray methods is returned. Each NSArray message sent to the collection proxy object will result in some combination of -countOf<Key>, -objectIn<Key>AtIndex:, and -<key>AtIndexes: messages being sent to the original receiver of -valueForKey:. If the class of the receiver also implements an optional method whose name matches the pattern -get<Key>:range: that method will be used when appropriate for best performance.
4 (introduced in Mac OS 10.4). Otherwise (no simple accessor method or set of ordered set or array access methods is found), searches the class of the receiver for a threesome of methods whose names match the patterns -countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class). If all three such methods are found a collection proxy object that responds to all NSSet methods is returned. Each NSSet message sent to the collection proxy object will result in some combination of -countOf<Key>, -enumeratorOf<Key>, and -memberOf<Key>: messages being sent to the original receiver of -valueForKey:.
5. Otherwise (no simple accessor method or set of collection access methods is found), if the receiver's class' +accessInstanceVariablesDirectly property returns YES, searches the class of the receiver for an instance variable whose name matches the pattern _<key>, _is<Key>, <key>, or is<Key>, in that order. If such an instance variable is found, the value of the instance variable in the receiver is returned, with the same sort of conversion to NSNumber or NSValue as in step 1.
6. Otherwise (no simple accessor method, set of collection access methods, or instance variable is found), invokes -valueForUndefinedKey: and returns the result. The default implementation of -valueForUndefinedKey: raises an NSUndefinedKeyException, but you can override it in your application.
Compatibility notes:
- For backward binary compatibility, an accessor method whose name matches the pattern -_get<Key>, or -_<key> is searched for between steps 1 and 3. If such a method is found it is invoked, with the same sort of conversion to NSNumber or NSValue as in step 1. KVC accessor methods whose names start with underscores were deprecated as of Mac OS 10.3 though.
- The behavior described in step 5 is a change from Mac OS 10.2, in which the instance variable search order was <key>, _<key>.
- For backward binary compatibility, -handleQueryWithUnboundKey: will be invoked instead of -valueForUndefinedKey: in step 6, if the implementation of -handleQueryWithUnboundKey: in the receiver's class is not NSObject's.
*/
- (nullable id)valueForKey:(NSString *)key;
Copy the code
The operation of assignment and value can be summarized as the following flow chart:
The assignment operation
SetValue: forKey: method:
Including accessInstanceVariablesDirectly method is the default return value is YES
Operating values
ValueForKey: methods:
verify
Verify the assignment operation
Here is a test code that verifies the assignment operation. In a real test, you can try to comment out different methods or member variables to see the output. It can be verified by testing that the output is consistent with the assignment flowchart summarized above.
@interface Cat: NSObject {@interface Cat: NSObject {@interface Cat: NSObject { NSInteger _isAge; NSInteger age; NSInteger isAge; } @property (nonatomic, assign) NSInteger age; - (void)setAge (NSInteger)age {_age = age; } // assignment, priority 2 - (void)_setAge:(NSInteger)age {_age = age; } / / the default true + (BOOL) accessInstanceVariablesDirectly {/ / test can be switch to false, to view an exception will melt then return true; } @endCopy the code
Validate value operation
Similarly, verifiable value operations are like flow charts.
@interface Cat: NSObject {@interface Cat: NSObject {@interface Cat: NSObject { NSInteger _isAge; NSInteger age; NSInteger isAge; } @property (nonatomic, assign) NSInteger age; @end@implementation Cat // Implementation Cat, priority 1 - (NSInteger)getAge {return _age; } // Value operation, priority 2 - (NSInteger)age {return _age; } // Value operation, priority 3 - (NSInteger)isAge {return _age; } // Value operation, priority 4 - (NSInteger)_age {return _age; } / / the default true + (BOOL) accessInstanceVariablesDirectly {/ / test can be switch to false, to view an exception will melt then return true; } @endCopy the code
Relationship with KVO
After checking the above code, you might wonder: does setting KVC for a property trigger KVO if it has a corresponding KVO listener?
Verify that KVC operations on attributes trigger KVO
Here is the test code for the Cat class:
@interface Cat: NSObject @property (nonatomic, assign) NSInteger age; - (void)willChangeValueForKey:(NSString *)key { NSLog(@"-----willChangeValueForKey: begin"); [super willChangeValueForKey:key]; NSLog(@"-----willChangeValueForKey: end"); } - (void)didChangeValueForKey:(NSString *)key { NSLog(@"-----didChangeValueForKey: begin"); [super didChangeValueForKey:key]; NSLog(@"-----didChangeValueForKey: end"); } @endCopy the code
The actual trigger code:
@interface ViewController () @property (nonatomic, strong) Cat *cat; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.cat = [[Cat alloc] init]; self.cat.age = 2; / / 2 / / initialized to add KVO listening NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.cat addObserver:self forKeyPath:@"age" options:options context:@"Kitten"]; } // KVO - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change Context :(void *)context {if (context == @"Kitten") {NSLog(@"KVO listen %@, %@, %@", object, keyPath, change); } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } // touch began :(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event [self.cat setValue:@5 forKey:@"age"]; } // Remove KVO - (void)dealloc {[self.cat removeObserver:self forKeyPath:@"age"]; } @endCopy the code
Compile and run, click on the screen test, and the output is as follows:
The 2021-01-02 17:20:42. 972312 + 0800 KVO (90787-2033943) -- -- -- -- -- willChangeValueForKey: Begin 2021-01-02 17:20:42.972504+0800 KVO[90787:2033943] -----willChangeValueForKey: End 2021-01-02 17:20:42.972609+0800 KVO[90787:2033943] -----didChangeValueForKey: Begin 2021-01-02 17:20:42.972854+0800 KVO[90787:2033943] KVO monitor <Cat: 0x600003AF4240 >, age, {kind = 1; new = 5; old = 2; } 2021-01-02 17:20:42.972989+0800 KVO[90787:2033943] -----didChangeValueForKey: endCopy the code
You can see that if you perform a KVC operation on a property directly, KVO will be triggered if KVO is added to the property. After all, the property KVC is used to set its setKey method first, and by looking at the objective-C KVO article we can see that operating on the setKey method triggers KVO.
Verify that a KVC operation with a member variable and no attributes triggers KVO
If the object does not have attributes but has member variables, there is no setKey method by default, so it is clear from the above flowchart that if the KVC operation is performed on the member variable, it is a direct assignment.
Modify the test code of Cat class
@interface Cat: NSObject {@interface Cat: NSObject {public NSInteger _age; } @implementation Cat - (void)willChangeValueForKey:(NSString *)key { NSLog(@"-----willChangeValueForKey: begin"); [super willChangeValueForKey:key]; NSLog(@"-----willChangeValueForKey: end"); } - (void)didChangeValueForKey:(NSString *)key { NSLog(@"-----didChangeValueForKey: begin"); [super didChangeValueForKey:key]; NSLog(@"-----didChangeValueForKey: end"); } @endCopy the code
Everything else in the ViewController class remains unchanged. Modify the initialization assignment:
self.cat = [[Cat alloc] init]; self.cat->_age = 3; // Change it to 3 to see the differenceCopy the code
Compile and run, click on the screen test, and the output is as follows:
The 2021-01-02 17:38:01. 537423 + 0800 KVO (91417-2045259) -- -- -- -- -- willChangeValueForKey: Begin 2021-01-02 17:38:01.537619+0800 KVO[91417:2045259] -----willChangeValueForKey: End 2021-01-02 17:38:01.537734+0800 KVO[91417:2045259] -----didChangeValueForKey: Begin 2021-01-02 17:38:01.537977+0800 KVO[91417:2045259] KVO monitor <Cat: 0x600003B14150 >, age, {kind = 1; new = 5; old = 3; } 2021-01-02 17:38:01.538102+0800 KVO[91417:2045259] -----didChangeValueForKey: endCopy the code
As you can see from the output, KVO can also be triggered if an object has no corresponding attributes and instead performs a KVC operation directly on its member variables.
Why is that? We know that KVO can be triggered automatically by setKey or can be called by itself:
[self willChangeValueForKey:@"xxx"];
[self didChangeValueForKey:@"xxx"];
Copy the code
Both methods trigger manually. So you can assume that if Apple did a KVC operation on the property, it would have called it at some point
[self willChangeValueForKey:@"xxx"];
[self didChangeValueForKey:@"xxx"];
Copy the code
These two methods. So KVO can be triggered, after all, look at the code output does print the relevant information.