In the previous article, you learned about key-value encoding KVC, a mechanism enabled by the NSKeyValueCoding informal protocol that objects use to provide indirect access to their properties. This article focuses on learning KVO, the foundation of KVO implementation is KVC key-value coding.
1.KVO protocol definition
The full name of KVO is key-value Observing. It translates to key-value Observing. Provides a mechanism to notify an object when other object properties have been modified.
-
The definition of KVO
The definitions of KVO are all implemented as extensions to NSObject (objective-C has an explicit NSKeyValueObserving class name-category). KVO is defined in Foundation, while the Foundation framework is not open source and can only be found in the official Apple documentation. See below:
KVO is covered in detail in the key-value observation programming guide. Key-value observation is a mechanism that allows an object to be notified when a specified property of another object changes. It is especially useful for communication between the model layer and the controller layer in an application.
To use KVO, you must first ensure that the observed object is KVO compliant. In general, if your object inherits from NSObject and you create properties the usual way, your object and its properties will automatically conform to KVO.
-
API provided by KVO
-
Listening to register
Method of use addObserver: forKeyPath: options: context: registered observer with the observed objects. The following steps must be performed to enable an object to receive key-value observation notifications for KVO compatible properties:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; Copy the code
The observer specifies an options parameter, options, and a context pointer, context, to manage all aspects of the notification.
-
Receive notifications
Within the observer to achieve observeValueForKeyPath: ofObject: change: context: to accept change notification message.
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context; Copy the code
-
Remove the monitor
Use method – (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; Remove observer
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; Copy the code
-
2. The use of KVO
1. Listen optionoption
Monitor option is defined by the enumeration NSKeyValueObservingOptions:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) { NSKeyValueObservingOptionNew = 0x01, NSKeyValueObservingOptionOld = 0 x02, NSKeyValueObservingOptionInitial API_AVAILABLE (macos (10.5), the ios (2.0), Tvos watchos (2.0), (9.0)) = 0 x04, NSKeyValueObservingOptionPrior API_AVAILABLE (macos (10.5), the ios (2.0), watchos (2.0), Tvos (9.0)) = 0 x08}Copy the code
Options affect both the content of the change dictionary provided in the notification and the way the notification is generated.
-
NSKeyValueObservingOptionNew
Listen for the new value of the property, as shown below:
-
NSKeyValueObservingOptionOld
Listen for the old value of the attribute, as shown in the following figure:
-
NSKeyValueObservingOptionInitial
When an observer is added, a notification is immediately sent to the observer, as shown in the following figure:
-
NSKeyValueObservingOptionInitial
Each time a property is changed, an advance notification is sent to the observer before the change notification is sent, which corresponds to the time when -willChangEvalueForKey: is triggered. In this way, two notifications are actually sent each time a property is modified.
2. Context PointersContext
AddObserver: forKeyPath: options: context: in the message context pointer containing arbitrary data, the data will be passed back to the observer in the corresponding change notification. You can specify NULL and rely entirely on the key path string to determine the source of the change notification, but this approach can cause problems for objects where the superclass is also observing the same key path for different reasons.
In the following example, LGStudent inherits from LGPerson and sets the name parent row of both objects. By adding the context pointer context, you can filter where notifications are received. See below:
3. Use techniques
-
The same object is registered with the same genus twice
Can call addObserver: forKeyPath: options: context: this way, will be the same object is registered for the same property of observer (all the parameters can be exactly the same). In this case, even if all parameters are the same, the newly registered observer will not replace the original observer, but will coexist. This way, when the property is modified, both listens respond.
Consider the following example:
You can see that KVO invokes a listen processing operation for each registration. So multiple calls to the same registration operation will result in multiple observers.
-
Remove observer
Through the above cases, can be concluded that the observer is no longer needed to monitor attribute changes, you must call removeObserver: forKeyPath: or removeObserver: forKeyPath: context: ways to remove the observer, the two method statement is as follows:
- (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)contextCopy the code
These methods remove the observer based on the parameters passed (mainly keyPath and context). Removing observers avoids the clutter of listening callbacks and maintains good code quality.
Note that if the observer does not listen to the keyPath property, calling both methods will throw an exception, as shown in the following figure:
Therefore, we must make sure that the observer is registered before we can call the remove method. So what if we forget to call the remove Observer method? Will collapse.
When an observation is added, neither object (i.e., the observer object and the object to which the attribute belongs) is retained. However, after the object is released, the related listening information still exists. In fact, apple’s official website also gives relevant instructions, see below:
- If you are not already registered as an observer, asking to be removed as an observer causes
NSRangeException
. - Observers do not automatically delete themselves when unallocated. 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, you want to make sure that the observer removes itself before it disappears from memory.
- The protocol does not provide a way to ask whether the object is the observer or the observed. So when building your code, avoid release-related errors.
- If you are not already registered as an observer, asking to be removed as an observer causes
-
Automatic listening and manual listening
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key Copy the code
By default, this method returns YES, meaning that all properties in any class can be listened on by default, which can be understood as automatic listening. In this mode, when we change the value of a property, KVO automatically calls the following two methods:
- (void)willChangeValueForKey:(NSString *)key - (void)didChangeValueForKey:(NSString *)key Copy the code
During development, you may not need to listen on all properties, but only selectively observe some properties. At this time + (BOOL) automaticallyNotifiesObserversForKey: (nsstrings *) key method to return NO, then you need to manually monitor properties. See the following code:
/ / automatic monitor switch - off + (BOOL) automaticallyNotifiesObserversForKey: (nsstrings *) key {return NO; } - (void)setName:(NSString *)name{ [self willChangeValueForKey:@"name"]; _name = name; [self didChangeValueForKey:@"name"]; }Copy the code
If you want to listen for changes to the name property of the Person object, you need to add the willChangeValueForKey and didChangeValueForKey methods to the setter method. The two methods must be paired, otherwise they will not work.
But if we want to control some of the details of notification delivery ourselves, we can enable manual control mode. Manual control notifications provide more precise control over KVO by controlling how and when notifications are sent to observers. This way you can reduce unnecessary notifications, or you can combine multiple changes into a single change.
At the same time through + automaticallyNotifiesObserversForKey: method can set the object in which attribute need manual processing, so can be handled automatically. See the following example:
-
Ensure that the property changes send notifications
If you want to minimize unnecessary notifications by sending them only when the property value is actually modified, you can do this as follows:
- (void)setNick:(NSString *)nick{ if (nick != _nick){ [self willChangeValueForKey:@"nick"]; _nick = nick; [self didChangeValueForKey:@"nick"]; } } Copy the code
If we change an instance variable (such as _nick) outside of the setter method, and we want the change to be heard by an observer, we need to do the same thing as in the setter method. This also relates to a common problem we encounter inside a class, when to access an attribute value using a property (self.nick) versus an instance variable (_nick). The general advice is to use instance variables when getting attribute values; When setting a property value, use the setter method whenever possible to ensure that the property is KVO. Of course, performance is also a concern, and setting values with instance variables is much better than setting values with attributes.
-
Multiple attribute dependency
In some scenarios, one property we are listening for may depend on changes in several other properties, resulting in a change in the computed property regardless of which of the dependent properties has changed. For this one-to-one relationship, we need To do two steps. The first step is To determine the relationship between the computed property and the dependent property. If we define a fullName property in the Person class, its getter method is defined as follows:
- (NSString *)fullName { return [[NSString alloc] initWithFormat:@"he's full name is :%@ , %@", self.name, self.nick]; } Copy the code
Having defined this dependency, we need to tell KVO in some way that when our dependent property changes, we will send a notification that the fullName property has changed. At this point, we need to rewrite NSKeyValueObserving agreement keyPathsForValuesAffectingValueForKey: method, this method returns the is a collection of objects, which influence the key contains the specified properties depend on the properties of the string. So for the fullName attribute, the implementation of this method is as follows:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{ NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"fullName"]) { NSArray *affectingKeys = @[@"name", @"nick"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } Copy the code
The fullName listener looks like this:
-
Collection property
One thing to understand about KVO for sets is that KVO is designed to look at relationships, not sets. For an immutable set attribute, we listen to it more as a whole, but cannot listen to the change of a certain element in the set. In the case of a mutable set attribute, it is actually used as a whole to listen for changes to it as a whole, such as adding, deleting, and replacing elements.
For the overall change of the listening set, see the following example:
What if you want to listen for changes to the data in the collection, such as adding, deleting, and replacing elements? Adding elements to a mutable array has no effect. See below:
We know that the KVO key-value listening implementation is based on KVC. Let’s take an array as an example. We have a dateArray array property in our Person class. If we want to respond to all of the dateArray methods, we need to implement the following:
So for mutable collections, instead of using valueForKey: to get the object, we use the following method:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; Copy the code
Add, modify and replace the elements in the mutable array respectively. The running result is shown in the following figure:
A problem has been found where kind has changed and the output values are 2, 3, 4. This is because the KVO mechanism can put detailed changes into the change dictionary when the set changes.
Supplementary: Set (Set) also has a corresponding Set of methods to implement Set proxy objects, including unordered sets and ordered sets; Dictionaries do not, and listening on dictionary attributes can only be handled as a collation.
If we want to manually control the sending of collection property messages, we can use several methods mentioned above, namely:
-willChange:valuesAtIndexes:forKey: -didChange:valuesAtIndexes:forKey: Or - willChangeValueForKey: withSetMutation: usingObjects: - didChangeValueForKey: withSetMutation: usingObjects:Copy the code
Make sure you turn off the automatic notification first, or KVO will be sent twice for every change.
-
Changes in the dictionary
Object must implement the observer – observeValueForKeyPath: ofObject: change: context: method, to handle property modification notification accordingly. This method is declared as follows:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context Copy the code
The third parameter, often called a Change Dictionary, records the changes in the properties being listened to. This dictionary contains values that vary depending on the options parameter we set when adding observers, and contains information about the properties that have been modified. We can use the following keys to obtain the information we want:
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM; /* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information. */ FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey; FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey; FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey; FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey; FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE (macos (10.5), the ios (2.0), Tvos watchos (2.0), (9.0));Copy the code
Where the value of NSKeyValueChangeKindKey is taken from NSKeyValueChange and its value is defined by the following enumeration:
Enum {// Set a new value. The property being listened on can be an object, a one-to-one or one-to-many relationship property. NSKeyValueChangeSetting = 1, // indicates that an object is inserted into the one-to-many relationship attribute. Said NSKeyValueChangeInsertion = 2, / / an object is removed from the attribute of a one-to-many relationship. NSKeyValueChangeRemoval = 3, / / an object in a one-to-many relationship attribute is replaced NSKeyValueChangeReplacement = 4}; typedef NSUInteger NSKeyValueChange;Copy the code
3.KVO implementation principle
With the capabilities provided by NSKeyValueObserving in mind, let’s look at the implementation mechanism of KVO to understand KVO more deeply. KVO is not open source, so we cannot analyze its implementation at the source level.
We got to the bottom of some of the mystery from the notes on Apple’s website. See below:
Automatic key-value observation is achieved using a technique called ISA-Swizzling. The ISA pointer points to the class of the object that maintains the 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.
So here’s where to explore: What intermediate class is this ISA pointing to? Kvo looks at the setter method, what does the setter method do, and whose setter method is it calling? Is the intermediate class destroyed when the listener is removed?
With these questions in mind, the principle of KVO is explored.
-
Look for the intermediate class NSKVONotifying_LGPerson
First, set a breakpoint to track the class to which the Person object isa pointer points, as shown in the following figure:
Before the listening was added, the class for the Person object was LGPerson. After the listening is added, the class to which the Person object ISA points is NSKVONotifying_LGPerson. This class should be the intermediate class mentioned in the website.
So when was this intermediate class created? We call addObserver: forKeyPath: options: context: before method, obtain NSKVONotifying_LGPerson this class, found this class does not exist. See below:
Note that this class should be dynamically generated at runtime through Runtime.
-
The relationship between NSKVONotifying_LGPerson and LGPerson
What does this intermediate class, NSKVONotifying_LGPerson, have to do with LGPerson? Through LLDB debugging, print the address of NSKVONotifying_LGPerson class, get its memory space, found that the parent class of NSKVONotifying_LGPerson class is LGPerson class.
So, NSKVONotifying_LGPerson is a subclass of LGPerson.
-
Methods provided by intermediate classes
The intermediate class NSKVONotifying_LGPerson has been found and is a subclass of LGPeron, so what methods does NSKVONotifying_LGPerson provide? Provides the following helper method to get a list of methods in a class.
**- (void)printClassAllMethod:(Class) CLS {unsigned int count = 0; Method *methodList = class_copyMethodList(cls, &count); for (int i = 0; i<count; i++) { Method method = methodList[i]; SEL sel = method_getName(method); IMP imp = class_getMethodImplementation(cls, sel); NSLog(@"%@-%p",NSStringFromSelector(sel),imp); } free(methodList); }Copy the code
In the call addObserver: forKeyPath: options: context: method, call the auxiliary method, see NSKVONotifying_LGPerson class what function. See below:
Found that the intermediate class overrides four methods of the parent class. These are setNickName, class, dealloc, and _isKVOA.
-
Object when ISA is fixed
Through the above analysis, we found the call addObserver: forKeyPath: options: context: method, the object of the isa pointed to a middle class, so the isa and re-execute LGPerson class?
Here it’s easy to think of – (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; Method, which is when you remove the listen. Verify below, see the following figure:
In the call – (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; Method, the object’s ISA pointer is redirected to the LGPerson class.
It is also found that after the destruction of the observer is completed, the intermediate class still exists and has not been destroyed. (To prepare for the next use, performance considerations, to avoid repeated creation) see the following figure:
-
The action of setter methods in intermediate classes
What’s the setter method doing here? Is it listening to a property or a member variable? NickName (” nickName “, name (” nickName “, name (” nickName “, name (” nickName “), name (” nickName “, name (” nickName “)) “)
KVO actually listens for properties through setter methods. We can analyze the underlying calling procedure by listening on the nickName member variable. See below:
We can see that when calling setNickName, we are actually calling the following process:
- Foundation _NSSetObjectValueAndNotify
- Foundation -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
- Foundation -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
-
conclusion
Objective-c relies on a powerful run time mechanism to implement KVO. When we first observe the properties of an object, Run Time creates a new subclass that inherits from the object’s class. In this new subclass, it overwrites the setters for all the observed keys and then points the object’s ISA pointer to the newly created class(this pointer tells the Objective-C runtime what type an object is). So the instance object magically becomes an instance of the new subclass. When you do that, when you call the setter to change the property, it’s this intermediate subclass that you’re doing that to. But the underlying layer still synchronizes the state of the operation on the intermediate class to the original object. After listening removal, the object’s ISA reverts to the original class.