preface
“This is the 14th day of my participation in the August More Text Challenge. For details, see: August More Text Challenge.”
Resources to prepare
Official Apple Document
KVO
Basic introduction to
KVO, full name key-value Observing, is an implementation of the Objective-C design pattern for observers. KVO provides a mechanism for objects to be notified of changes and to act accordingly; Without adding any extra code to the observed object, you can use the KVO mechanism. KVO is based on KVC, so you must first understand KVC, which was analyzed in the previous article.
What is the difference between KVO and KVC?
-
KVC is a key-value encoding mechanism that is initiated by the NSKeyValueCoding informal protocol. After the object is created, it can dynamically assign values to the object attributes.
-
KVO is key-value observation, a listening mechanism. The object is notified when its properties have been modified. Therefore, KVO is based on KVC, on the basis of dynamic changes of the attribute monitoring;
2. What is the difference between KVO and NSNotificatioCenter
-
Similarities: Firstly, they are both observer mode, both are used for listening, and both can implement one-to-many operations.
-
Difference:
- Kvo: can only be used to listen to the changes of object attributes, can send messages controlled by the system, but also can automatically record the old and new value changes;
NSNotificatioCenter
; You can register anything you are interested in, which is controlled by the developer and can only record parameters passed by the developer.
API
introduce
API usage, divided into registered observer, receive change notification, remove observer:
1. Register observer: Use this method to register an observer to the observed object
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
Copy the code
Parameter analysis:
-
The first observer is the object of the listener that is added and notified when the properties of the listener change.
-
The second keyPath is the listening property, you can’t pass nil;
-
The third options specifies the timing of the notification and the key value in change:
NSKeyValueObservingOptionNew
The value before the change is supplied to the processing methodNSKeyValueObservingOptionOld
Supply the changed value to the processing methodNSKeyValueObservingOptionInitial
The initialized value is provided to the handler, which is called once registered. Usually it takes the new value, not the old value.NSKeyValueObservingOptionPrior
Call in 2 times. Before and after the value changes
-
The fourth is context
2. Receive change notifications: Implemented within the observer to receive change notification messages
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
Copy the code
3. Remove observers: Use methods to unregister observers when they are no longer supposed to receive messages. At least call this method after the observer is freed from memory
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
Copy the code
API
introduce
context
The use of
Registered observer method addObserver: forKeyPath: options: context: in the context can pass in any data, and can receive the data in the monitoring method. You can also set the context to NULL, relying entirely on the keyPath string to determine the source of the change notification. If you’re looking at different properties within the same class, you need to make complex distinctions in the receiving area; It can also cause problems with objects whose parents are also looking at the same key path for different reasons.
Use a case to make it more intuitive:
- (void)viewDidLoad { [super viewDidLoad]; self.person = [DMPerson new]; [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL]; [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if (object == self.person) { if ([keyPath isEqualToString:@"nick"]) { NSLog(@"nick Changed the \ n % @ ", change); } else if ([keyPath isEqualToString:@"name"]) {NSLog(@"name "changed \n%@",change); } } } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick]; self.person.name = [NSString stringWithFormat:@"%@*",self.person.name]; }Copy the code
Observe the LGPerson object name and Nick and set the context to NULL. In the observeValueForKeyPath, we need to determine whether the object is LGPerson or not. Let’s see if keyPath is Nick or name. And the printed result is the same, as follows:
Next, let’s set the context parameter:
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;
Copy the code
At registration, add the context again:
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
Copy the code
At the point of receiving the message, we can judge directly from the context:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {if (context == PersonNickContext) {NSLog(@" changed by context Nick \n%@",change); } else if (context == PersonNameContext) {NSLog(@" changed by context name \n%@",change); }}Copy the code
Now look at the print:
The result is the same, which means that the context is actually a marker, making it easier and more straightforward to determine which attribute is being called back. Context makes it a safer, more extensible approach, and improves the efficiency of notification resolution, while also ensuring that notifications received are sent to observers and not to the superclass.
Remove observer
When use KVO, if don’t need to use, will remove the observer, is also perform removeObserver: forKeyPath: context: method, Then people will no longer receive observeValueForKeyPath: ofObject: change: context: specify the keyPath and object of any message. And this is usually in the dealloc method. So if we don’t remove it, is that ok?
We can write a simple page that gives us a way to jump to the second screen, and then write the following code in the second screen time:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.nick = @"hellow world";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"LGDetailViewController :%@",change);
}
Copy the code
Then each time you go to the second screen, you tap the screen and go back to the first screen, and repeat a few more times. Look at the print:
It prints every time, no big deal.
If we replace the LGPerson object with LGStudent(which is a singleton), look at the result:
- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor orangeColor]; self.student = [LGStudent shareInstance]; // weak observer [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.student.name = @"hello word"; } #pragma mark - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"LGDetailViewController :%@",change); }Copy the code
Follow the steps above and repeat the following steps:
An error will be reported. Why? We can find the answer we want in the official document:
**An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from When removed, the observer does not automatically delete itself, so when the property changes, the observer will continue to be notified, but the observer has been released, resulting in an exception in memory access. Therefore, it is necessary to ensure that the observer is removed when it is destroyed.Copy the code
-
When we first went into LGDetailViewController, we registered the observer, clicked on the screen to trigger the callback, no problems, and then went back to the top page
-
When we enter LGDetailViewController for the second time, the observer we registered for the first time will be destroyed, but because LGStudent is a singleton, it will not be released, so we will still send a callback message, resulting in memory access exception and application crash. This calls dealloc to release.
So the observer does not automatically remove itself when it is removed. The observed object continues to send notifications, ignoring the state of the observer. However, a change notification sent to a released object triggers a memory access exception just like any other message. Therefore, make sure that the observer removes itself before it disappears from memory.
KVO
Automatic/manual trigger
When we use KVO, in general, the default is automatic monitoring mode, the manual monitoring and when we want to change into the model, we need to be monitored objects automaticallyNotifiesObserversForKey method:
+ (BOOL) automaticallyNotifiesObserversForKey: (nsstrings *) key {/ / may, according to different key values, If ([key isEqualToString:@" Nick "]) {return YES; } return NO; }Copy the code
If it returns YES, it will be in automatic mode, and if it returns NO, it will be all manual listening, which is why we touched the screen in the above case and there was NO response.
To implement manual observer notification, please willChangeValueForKey: called before changing the value and didChangeValueForKey: called after changing the value.
- (void)setNick:(NSString *)nick {
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
Copy the code
It will be listened for manually.
Observe attributes that are influenced by multiple factors
Multiple factor influences, or one-to-many relationships, can be monitored for changes in multiple attributes by registering a KVO observer.
Let’s use a common download progress as an example:
Download progress = Number of current downloads currentData/Total downloads totalData;
So any change in currentData or totalData will affect the download progress.
Observe the totalData and currentData properties respectively. When the value of one of the properties changes, calculate the current downloadProgress.
In the observed object LGPerson keyPathsForValuesAffectingValueForKey: method of merging currentData and totalData properties of observation;
Let’s describe it through a case:
- in
LGPerson
Code in:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{ NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"downloadProgress"]) { NSArray *affectingKeys = @[@"totalData", @"writtenData"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } / / automatic switch + (BOOL) automaticallyNotifiesObserversForKey: (nsstrings *) key {return YES; } - (NSString *)downloadProgress{ if (self.writtenData == 0) { self.writtenData = 10; } if (self.totalData == 0) { self.totalData = 100; } return [[nsstrings alloc] initWithFormat: @ "% f", 1.0 f * self writtenData/self totalData]; }Copy the code
- In the secondary interface:
- (void)viewDidLoad { [super viewDidLoad]; self.person = [LGPerson new]; / / download progress = downloaded/total download [self. The person addObserver: self forKeyPath: @ "downloadProgress options: (NSKeyValueObservingOptionNew)" context:NULL]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.person.writtenData += 10; self.person.totalData += 1; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); } - (void)dealloc { [self.person removeObserver:self forKeyPath:@"nick" context:NULL]; }Copy the code
If we look at the print again, we touched the screen three times:
Through keyPathsForValuesAffectingValueForKey method, downloadProgress totalData and writtenData correlation of two factors, Through setByAddingObjectsFromArray associated, so every time totalData or writtenData change, downloadProgress changed automatically inform the observer. The only reason it printed three times the first time was because we set totalData to 100 when it was 0, so it was called one more time.
Listening for mutable arrays
We can use KVO to listen for mutable arrays. Because KVO is based on KVC, we can use the three different proxy methods defined for accessing collection objects as described in KVC’s documentation:
-
The first is: mutableArrayValueForKey: and mutableArrayValueForKeyPath:. They return a proxy object that behaves like an NSMutableArray object;
-
The second is: mutableSetValueForKey: and mutableSetValueForKeyPath:. They return a proxy object that behaves like an NSMutableSet object;
-
The third is: mutableOrderedSetValueForKey: and mutableOrderedSetValueForKeyPath:. They return a proxy object that behaves like an NSMutableOrderedSet object.
The case code is as follows:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
if(self.person.dateArray.count == 0){
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
} else {
[[self.person mutableArrayValueForKey:@"dateArray"] removeObjectAtIndex:0];
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@",change);
}
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"dateArray"];
}
Copy the code
Run the print result:
Note: Kind indicates the type of key change. When addObject is executed, the kind print value is 2. When you run removeObjectAtIndex, the kind print value is 3
Let’s look at the enumeration information for kind’s definition:
/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information. */ typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, / / assignment NSKeyValueChangeInsertion = 2, / / insert NSKeyValueChangeRemoval = 3, / / remove NSKeyValueChangeReplacement = 4, / / replace};Copy the code
KVO
Underlying principle of
According to the official document, we can know:
Automatic key-value observation is achieved using a technique called ISA-Swizzling.
-
The ISA pointer, as the name suggests, points to the object’s class, which holds a schedule table. The schedule consists primarily of Pointers to methods implemented by the class, as well as other data.
-
When an observer registers an object’s property, the ISA pointer to the observed object is modified to point to the intermediate class rather than the actual class. Therefore, the value of the ISA pointer does not necessarily reflect the actual class of the instance.
-
You should not rely on an ISA pointer to determine the members of a class. Instead, use the class method to determine the class of the instance object.
isa
The change of the
Let’s debug and print the code to see what happens:
With LLDB debugging, the class object to which the printed Person belongs changed before and after adding the registered KVO observer addObserver. We know that the relationship between an instance object and a class is actually that the isa of the instance object points to the class object. So here we can infer that self.person, after calling the addObserver method, has gone from being an instance of class LGPerson, to being an instance of NSKVONotifying_LGPerson.
NSKVONotifying_x
Creation time of
With the case print above, we need to verify that the NSKVONotifying_LGPerson class already exists, or that KVO was temporarily generated.
Note: objc_getClass is an API for Runtime, so you must import header files, #import <objc/runtime.h>.Copy the code
By printing, you know that when KVO is added, the NSKVONotifying_LGPerson class is temporarily generated and the ISA of the instance object is pointed to that class. So what’s the parent class of NSKVONotifying_LGPerson? Let’s debug through LLDB again and see:
Now, we know that the parent class of NSKVONotifying_LGPerson, turns out, is LGPerson.
It can also be printed with the following code:
NSLog(@"NSKVONotifying_LGPerson Superclass: %@",class_getSuperclass(objc_getClass("NSKVONotifying_LGPerson")));Copy the code
NSKVONotifying_x
The methods in
Next, iterate over all the methods in the NSKVONotifying_LGPerson class as follows:
- (void)viewDidLoad { [super viewDidLoad]; self.person = [[LGPerson alloc] init]; [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; unsigned int intCount; Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LGPerson"), &intCount); for (unsigned int intIndex=0; intIndex<intCount; intIndex++) { Method method = methodList[intIndex]; NSLog(@"SEL: %@, IMP: %p",NSStringFromSelector(method_getName(method)), method_getImplementation(method)); }}Copy the code
Printed results:
NSKVONotifying_LGPerson overwrites the setNickName method of the parent class. It also overwrites the class, dealloc, and _isKVOA methods of NSObject.
rewriteclass
Purpose of the method
Purpose: To hide KVO generated intermediate classes.
We call the class method of the intermediate class and return the address of the original class object. We print the comparison between before and after KVO is added. The code looks like this:
- (void)viewDidLoad { [super viewDidLoad]; self.person = [[LGPerson alloc] init]; Class cls = self.person.class; NSLog(@" before adding KVO observer: %s, %p, %@", object_getClassName(self.person), &cls, CLS); [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; cls = self.person.class; NSLog(@" after adding KVO observer: %s, %p, %@", object_getClassName(self.person), &cls, CLS); }Copy the code
Look at the print again:
As a result, the class method overwritten by the intermediate class is still LGPerson class, as if everything KVO did did not exist.
rewritedealloc
Purpose of the method
Purpose: To redirect the isa of the instance object to the original class object after the observer is removed.
To see what happens to the instance object after the removeObserver method is called, look at the code below:
Looking at the print results, we can see that this enables the class method of the intermediate class, along with the dealloc method, to successfully replace the ISA pointing to the instance object without any trace to the developer.
NSKVONotifying_x
Whether the class was destroyed
According to the analysis above, when the observer is removed, the isa of the instance object points to the original class object. At this point, the intermediate class NSKVONotifying_LGPerson has completed its task. Will it be destroyed?
So let’s add the code that prints the NSKVONotifying_LGPerson class information at click screen and dealloc. The code is as follows:
- (void) Touches began :(NSSet< touches *> *) Touches withEvent:(NSSet< touches *> *)touches withEvent: %@", objc_getClass("NSKVONotifying_LGPerson")); }Copy the code
According to the print, the intermediate class is not destroyed directly when the observer is removed. You might consider adding observers again, which you can reuse.
rewrite_isKVOA
Purpose of the method
Purpose: To mark whether it is an intermediate class generated when KVO is added.
Using the KVC method [self.person valueForKey:@”isKVOA”], print the isKVOA of the original and intermediate classes as follows:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@" before adding KVO observer: %s, _isKVOA: %@", object_getClassName(self.person), [self.person valueForKey:@"isKVOA"]); [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL]; NSLog(@" add KVO observer: %s, _isKVOA: %@", object_getClassName(self.person), [self.person valueForKey:@"isKVOA"]); }Copy the code
Let’s look at the print:
It’s labeled, 0 before adding the KVO observer, 1 after adding the KVO observer.
rewritesetter
Purpose of the method
Overriding the setter method, the intermediate class is responsible for calling the KVO related system function, and then calling the parent setter method, ensuring that the property assignment in the original class is successful. When all is said and done, the intermediate class continues to call system functions and finally calls KVO’s callback notifications.
KVO
You can only listen to properties
So let’s compare the results by adding KVO to the property variable with or without the setter method. The code looks like this:
We know that there is no setter for the name property. NickName has a setter method, and nickName, according to the analysis above, overrides the setter method.
- (void)viewDidLoad { [super viewDidLoad]; self.person = [[LGPerson alloc] init]; [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL]; unsigned int intCount; Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LGPerson"), &intCount); for (unsigned int intIndex = 0; intIndex < intCount; intIndex++) { Method method = methodList[intIndex]; NSLog(@"SEL: %@, IMP: %p",NSStringFromSelector(method_getName(method)), method_getImplementation(method)); } } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.person->name = @"changeNewName"; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ NSLog(@"%@",change); } - (void)dealloc { [self.person removeObserver:self forKeyPath:@"name"]; }Copy the code
The result of running the print execution method:
According to the analysis of the results, there is no setter method, and clicking on the screen does not receive the KVO listen callback, but the value is changed. So it follows that KVO can only listen on attributes, not member variables. It is also possible to deduce that changes to the name in the KVO generated class affect the original class.
setter
Method invocation flow
- Step 1: Execute to breakpoint;
- Step 2: Pass
lldb
Debug, enterwatchpoint set variable self->_person->_nickName
Command; - Step 3: Release the breakpoint and continue running, then click the screen;
- Step 4: Pass
lldb
Debug, input BT to view stack information.
Call to LGPerson setNickName (); call to LGPerson setNickName (); call to Foundation();
-
Foundation
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]:
-
Foundation
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]:
-
Foundation
_NSSetObjectValueAndNotify:
In the assembly, _NSSetObjectValueAndNotify call mainly as follows:
Next, call LGPerson’s setNickName method.
NickName (nickName) : nickName (nickName) : nickName (nickName) : nickName (nickName) : nickName (nickName)
-
Foundation`NSKeyValueDidChange:
-
Foundation`NSKeyValueNotifyObserver:
The last call KVO correction notice: observeValueForKeyPath: ofObject: change: context: