What is KVO?

KVO is a mechanism based on KVC that can notify objects of changes in property values of other objects.

1.1. Register KVO

You must perform the following steps to enable an object to receive key-value observation notifications for KVO compatible properties:

  • Method of useaddObserver:forKeyPath:options:context:Registers the observer with the observed object.
  • observeValueForKeyPath:ofObject:change:context:Implement this method inside the observer to receive change notification messages.
  • removeObserver:forKeyPath:This method is used to unregister an observer when the observer no longer needs to receive messages. Call this method at least before releasing the observer from memory.
  • removeObserver:forKeyPath:context:When we register an observer, ifcontextParameters forNULLShould use this method to remove, it is safer.

1.2. Context Parameter Description

AddObserver: forKeyPath: options: context: context parameters of the method in the corresponding observeValueForKeyPath: ofObject: change: context: back to the observer. You can specify NULL to determine the source of the view property by relying on keyPath, but when multiple objects have the same view property, it is not convenient to judge by keyPath.

A more secure and scalable way to do this is to use context.

Creation of the context pointer.

The static void * PersonAccountBalanceContext = & PersonAccountBalanceContext; The static void * PersonAccountInterestRateContext = & PersonAccountInterestRateContext;Copy the code

Receive notifications from KVO

Observed when the attribute value is changed, the observer will receive a observeValueForKeyPath: ofObject: change: context: message. All observers must implement this method.

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
 
    if (context == PersonAccountBalanceContext) {
        // Do something
 
    } else if (context == PersonAccountInterestRateContext) {
        // Do something
 
    } else{ // Any unrecognized context must belong to super [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; }}Copy the code

When we use the context parameter when registering an observer, we can then use the context at the place where the notification is received to distinguish which object’s property triggered the notification callback.

If a context is passed with NULL when registering an observer, a comparison is made using keyPath to determine which object’s attributes were changed.

In any case, the observer should always observeValueForKeyPath: ofObject: change: context: in its unable to identify the context (or in the simple case, is any keyPath) calls when the realization of the parent class, because it means that the parent class has registered a notification.

If notification delivery to the top of the class hierarchy, the NSObject thrown, NSInternalInconsistencyException because it is a programming error: a subclass cannot be used for the registration notice.

Removing KVO

Object to be observed by sending a removeObserver: forKeyPath: context: news, specify the observer, keyPath and context, you can delete the key value observer.

When removing an observer, keep the following in mind.

  • If an unregistered observer is removed, one will be raisedNSRangeExceptionException, you canremoveObserver:forKeyPath:context:The call is placed in a try/catch block to handle potential exceptions.
  • When the object is released, the observer is not automatically removed. If the object is not released, the observed object continues to send notifications. Like other objects, sending messages to the freed object raises a memory exception. To do this, make sure that the observer deletes itself before the object is released.
  • The protocol cannot ask whether an object is an observer or an observed. So that the code doesn’t have related errors. A typical approach is during observer initialization (for example, ininitOr in theviewDidLoadRegister as an observer during release (usually indealloc) unregister (make sure that the add and remove messages are properly paired and sorted) and unregister the object before it is freed from memory. .

Automatic notification versus manual notification

KVO default is automatic notification, that is, when we change the value of the attribute will automatically send a notification, we can have such kind of rewriting automaticallyNotifiesObserversForKey: method to control whether to enable automatic notification.

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return YES;
}
Copy the code
  • Return toYESIs to enable automatic notification for all properties of the object.
  • Return toNODisables automatic notification for all objects of that object.

We can enable or disable automatic notification for a property based on the Key.

In addition, the system generates unique methods to enable and disable automatic notification for specific attributes.

@interface Account : NSObject
@property (nonatomic, assign) double balance;
@property (nonatomic, assign) double interesRate;
@end
Copy the code

In the case of a property in Account, the compiler automatically generates two methods for us to control whether automatic notification is enabled for that property.

+ (BOOL)automaticallyNotifiesObserversOfBalance {
    return NO;
}

+ (BOOL)automaticallyNotifiesObserversOfInteresRate {
    return NO;
}
Copy the code

AutomaticallyNotifiesObserversForKey: priority is greater than a specific attribute of the generated method, if implemented automaticallyNotifiesObserversForKey: method, the specific properties of methods will not be invoked.

To implement manual observer notification, manually call willChangeValueForKey: before changing the value and didChangeValueForKey: after changing the value. Manual notification is implemented with the balance attribute.

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}
Copy the code

KVO of a mutable set

When we listen to objects whose properties are mutable collections or mutable arrays, we need to do something special if we want to be notified when the contents of the array or collection change.

  • usemutableArrayValueForKey:Method takes an array from an object and then operates on a mutable array. At this point, we are notified of the change in the contents of the array.
NSMutableArray *mArray = [self.account mutableArrayValueForKey:@"transactions"];
[mArray addObject:@"4"];
Copy the code
  • Mutable collections operate similarly to this, usingmutableSetValueForKey:.

Attribute dependence

When the value of an attribute depends on several other attributes, We can use keyPathsForValuesAffectingValueForKey: method or use the following naming keyPathsForValuesAffectingValueFor < Key > since to build relationships.

For example, a person’s full name depends on first and last names. The method to return the full name could be written as follows:

- (NSString *)fullName {
    return [NSString stringWithFormat:@"% @ % @",firstName, lastName];
}
Copy the code

We are listening externally for fullName and should trigger a callback when the value of firstName or lastName changes. There are two ways to create dependencies.

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName"The @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
Copy the code
+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName"The @"firstName", nil];
}
Copy the code

KVO implementation principle

KVO is implemented using the ISa-Swizzling technique, which simply changes the isa pointer to an object so that it points to an intermediate class instead of the real class. Therefore, the value of the ISA pointer does not reflect the actual class of the instance, so the class method should be used to determine the actual class of the object.

1.1. KVO verification

So let’s do a simple test. Now we have a Person class

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
Copy the code

We only look at the isa pointer to the output object before adding KVO and adding KVO respectively.

self.person = [Person new];
{
    Class cls = object_getClass(self.person);
    NSLog(@"% @", NSStringFromClass(cls));
}

[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
{
    Class cls = object_getClass(self.person);
    NSLog(@"% @", NSStringFromClass(cls));
}
Copy the code

The output is as follows

Person 2020-02-14 10:24:32.252254+0800 KVO Person 2020-02-14 10:24:32.252750+0800 KVO Person [23368:1376988] NSKVONotifying_PersonCopy the code

The isa pointer points to the Person class before KVO is added, and the isa pointer points to NSKVONotifying_Person after KVO is added. At this point we can conclude that after adding KVO, the object dynamically generates an NSKVONotifying_Person class for us at runtime and points the isa pointer to this object to the new class.

1.2. Inheritance of dynamic classes

We all know that in OC, all classes have a parent class, so let’s look at the inheritance of NSKVONotifying_Person.

Class cls = object_getClass(self.person);
NSLog(@"% @", NSStringFromClass(cls));

Class supCls = cls;
do {
    supCls = [supCls superclass];
    NSLog(@"% @", NSStringFromClass(supCls));
} while (supCls);
Copy the code

This code will print all the parent classes of the class.

[23558:1388700] NSKVONotifying_Person 2020-02-14 10:37:24.826718+0800 Person 2020-02-14 10:37:24.826840+0800 KVO Person 2020-02-14 10:37:24.826840+0800 KVO NSObject 2020-02-14 10:37:24.826945+0800 KVO Principle Exploration [23558:1388700] (NULL)Copy the code

After verification, we see that NSKVONotifying_Person directly inherits from Person.

1.3. Dynamic class method exploration

Let’s take a look at the write methods in this dynamically generated class. We use the Runtime API to output all the methods in this class and their implementation.

- (void)printClassAllMethod:(Class)cls {
    unsigned int count = 0;
    Method *methods = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        SEL methodSel = method_getName(method);
        IMP methodImp = method_getImplementation(method);
        NSLog(@"%@-%p", NSStringFromSelector(methodSel), methodImp);
    }
    free(methods);
}
Copy the code

To print out what methods are defined in this class, let’s look at NSKVONotifying_Person.

706699+0800 KVO Principle Exploration [24582:1444989]setName: -0x7FFF25721C7A 2020-02-14 11:43:09.706794+0800 KVO Principle Exploration [24582:1444989] class -0x7FFF2572073D 2020-02-14 [24582:1444989] Dealloc-0x7FFF257204A2 2020-02-14 11:43:09.706973+0800 KVO principle exploration [24582:1444989] _isKVOA- 0x7FFF2572049ACopy the code

We found that it overrides three methods and customizes one method, and most importantly it overrides setter methods for properties.

Here we also print out all the methods in the Person class.

[24582:1444989]. Cxx_destruct-0x108053ee0 2020-02-14 11:41:12.101732+0800 KVO Principle Exploration [24582:1444989] Name - 0x108053E70 2020-02-14 11:41:12.101854+0800 KVO Principle exploration [24582:1444989]setName:-0x108053ea0
Copy the code

Let’s take a look at what happens when you set the properties after adding KVO.

2020-02-14 11:41:12.101854+0800 KVO Principle Exploration [24582:1444989]setName:-0x108053ea0
Copy the code

The address of the setter method is the same as the calling address, which indicates that the setter method was called directly.

706699+0800 KVO Principle Exploration [24582:1444989]setName:-0x7fff25721c7a
Copy the code

So when you assign a property to an object after adding KVO to it, you call the method overridden in the dynamic class. In this method we find that it calls willChangeValueForKey: and didChangeValueForKey:, which are used to send notifications.

2. Principle summary

  • Listener monitoringPersonObject to create a dynamic subclass of the Person classNSKVONotifying_PersonAnd willPersonObject to redirect the ISA pointer to that subclass
  • The system will rewritePersonObject setter method. (Called separately before and after assignmentwillChangeValueForKeyanddidChangeValueForKeyTrace old and new values). Object assignment is handled by calling setter methods of the parent class.
  • whenPersonWhen the properties of the object change, the system notifies the listener and callsObserveValueForKey: ofObject: change: contextMethod can.

The problem. When KVO is added to our object, why is the Person class retrieved from the class method? Because NSKVONotifying_Person overrides the class method, it returns Person in this method. But object_getClass gets an ISA pointer, so calling object_getClass returns NSKVONotifying_Person.