Study harmoniously! Don’t be impatient!! I’m your old friend, Xiao Qinglong

preface

In previous articles, we explored the principles of KVC. This article will explore KVO. Although KVC and KVO look very similar, you should not confuse them

  • KVC: the Key – Value Coding
  • KVO: the Key – value observing

As usual, let’s start with KVO:

KVO is simple to use:

  1. Register (addObserver: forKeyPath: options: context:)

  2. Callback agent method (observeValueForKeyPath: ofObject: change: context:)

  3. Remove the observer (removeObserver: forKeyPath: context:, this need to remove, or it will because to a freed object sends a message and send)

Principle of inquiry

We all use it this way, but when we want to see the implementation, we can’t click to see the next step:

Don’t worry, at this point we can open the official KVO document provided by Apple dad:

The picture basically means:

  1. KVO provides a mechanism that allows an object to be notified when a specified property of another object changes.

  2. It can be the controller observing the value change of the data model;

  3. Model data A can observe the change of model data B (for example, model B is one of the attributes of model A).

  4. Model data can also monitor changes in its own data;

As for the second point, the document also gives a case: As one of the attributes of Person, Account has the attributes of balance and interest rate, and the Person object can monitor the changes of balance and interest rate of the Account object.

Of course, as for the change of account information, you can check regularly to know the change of information. But this is inefficient and not timely enough. At this time, KVO is just like a person occupying the side of the account, watching the account for 24 hours, and immediately sending a notice telling you that “Dude, your account has been changed, and the change information is XXX”. Finally, you need to manually unlisten when you don’t need to.

Use process and precautions of KVO

The figure above illustrates the implementation steps of KVO (Register, call back proxy, remove) :

  • addObserver:forKeyPath:options:context:

  • observeValueForKeyPath:ofObject:change:context:

  • removeObserver:forKeyPath:

Note that not all classes are KVO compliant for all attributes. By following the steps described in KVO Compliance, you can ensure that your classes are KVO compliant. In general, properties in the framework provided by Apple only comply with KVO if they are documented.

About the KVO around

  • The class must conform to the key-value encoding of the attribute, as described in Ensuring KVC conformance. KVO supports the same data types as KVC, including Objective-C objects and scalars and structures listed in scalars and structures support.

  • This class issues KVO change notifications for attributes.

  • The relevant Keys have been properly registered (see Registering Dependent Keys).

Check out alarm Dependent Keys

In many cases, the value of an attribute depends on the value of one or more other attributes in another object. If the value of an attribute changes, the value of a derived attribute should also be marked as changed. How you ensure that key-value observation notifications are published for these dependent properties depends on the cardinality of the relationship.

About the addObserver: forKeyPath: options: context:

Register an observer object to receive A KVO notification of the key path associated with the object receiving this message.

Parameters of the Options
  • NSKeyValueObservingOptionNew// Contains only the changed state information
  • NSKeyValueObservingOptionOld// Contains only the state information before the change
  • NSKeyValueObservingOptionInitial// In this mode, the observeValueForKeyPath agent is traversed twice, out of sync
  • NSKeyValueObservingOptionPrior// In this mode, the observeValueForKeyPath agent is traversed twice and synchronized

In general, we will use NSKeyValueObservingOptionNew and NSKeyValueObservingOptionOld, these two people familiar with the, don’t do this here.

The next test NSKeyValueObservingOptionInitial:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [SSJPerson new];
    self.person.weight = @"50kg";
    // Register the observer
    [self.person addObserver:self forKeyPath:@"weight" options:(NSKeyValueObservingOptionInitial) context:SSJPersonWeightContext];
    __block typeof(self)blockSelf = self;
    // Change content after 2 seconds
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"Start to change.");
        blockSelf.person.weight = @"52kg";
    });
    NSLog(@"\ n \ n interval ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ \ n");
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"Print change: %@",change);
    NSLog(@"End of demo ~");
}
Copy the code

Run:

First break point:

Second break point:

The next test NSKeyValueObservingOptionPrior:

First break point:

Second break point:

Description:

  • NSKeyValueObservingOptionInitial mode, will be registered immediately after the observation model performs a proxy method, return to the observed person’s initial state, when the observed attributes change, will call again, and return the altered state.

  • NSKeyValueObservingOptionInitial mode, when the observed attributes change, will call twice agent method, for the first time to return to the old data, return to the new data for the second time.

Parameters of the Context

Context: The context identifier, any data passed to the observer. A safer and more extensible approach is to use a context context to ensure that the notifications you receive are specific to your observer and not to the superclass. To put it simply, if the same message receiver is observed in different SSJPerson objects with different property values, then the observeValueForKeyPath will need to make several judgments to find the message to process, by setting different contexts as unique identifiers. Different messages can be located more efficiently. Such as:


SSJPerson *personA;
SSJPerson *personB;
static void *SSJPersonAWeightContext = &SSJPersonWeightContext;
static void *SSJPersonBNameContext = &SSJPersonBNameContext;;
- (void)viewDidLoad {
    [super viewDidLoad];

    personA = [SSJPerson new];
    personB = [SSJPerson new];
    [personA addObserver:self forKeyPath:@"weight" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:SSJPersonAWeightContext];
    [personB addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:SSJPersonBNameContext];
    personA.weight = @"12";
    personB.name = @"Fifty";
}
Copy the code

Parameters observer and keyPath

  • Observer: The object for which KVO notifications are registered. After observation of registration, the observer must implement observeValueForKeyPath: ofObject: change: context: proxy method.

  • KeyPath: The key-value pair path of the property to be viewed and cannot be nil.

About observeValueForKeyPath: ofObject: change: context:

Notifies the observed object when the value on the specified keyPath (keyPath) of the observed object changes.

parameter

  • KeyPath: key value pair path.

  • Object: indicates the observed object.

  • Change: dictionary type, containing changes to keyPath specified path values.

  • Context: The content passed in when the observer is registered.

About removeObserver: forKeyPath:

Sent through to the observation object removeObserver: forKeyPath: context: message, specify the observation object, key path and the context, you can delete the key value observer.

  • If you are not registered as an observer, requesting deletion as an observer causes an NSrange exception. You can put it in a try/catch block to handle potential exceptions.

Case study:

Error message successfully caught after adding try/catche processing:

  • Observers do not automatically delete themselves when unassigned. The observed object continues to send notifications regardless of the status of the observer. However, as with any other message, a change notification sent to a published object triggers a memory access exception. Therefore, you can ensure that the observer removes itself before it disappears from memory.

Case study:

Create an SSJPerson singleton as observed, and set the current controller as observer. The first time back to the previous layer, the controller is destroyed. But because of the list, and the fact that there are no observers that did not remove the first time, the observed will continue to send messages to the destroyed controller (it will only send messages to the destroyed controller when it changes, i.e. the second time it enters the control layer, when it enters the test03 method). Here’s a diagram for you to understand:

  • The protocol does not provide a way to ask whether an object is an observer or an observed. Construct code to avoid release-related errors. The typical pattern is to register as an observer during observer initialization (such as in init or viewDidLoad) and to unregister during unallocation (usually in Dealloc) to ensure proper pairing and orderly add and remove messages, and to ensure that the observer is unregistered before it is released from memory.

Remove is a non-existent observation mode, and will report exceptions. The official recommendation is to use try/cache to avoid application crashes due to exception information. The question is, there will always be mistakes, and how can we avoid them? Or, can you know in advance whether you can remove?

  • Write an NSObject class, replace the systemaddObserver:forKeyPath:options:context:Method, in the new method willobserandkeypathSave it to your own singleton. Before remove, obtain the corresponding information from the singleton and determine whether to remove according to the read information.removeDon’t forget the singleton after thatRemove corresponding data.

Observe several application scenarios for patterns

John needs to be notified when his working hours change.

  • The controller listens for changes to the data model:

  • Listening between models

Implementation details of KVO

Translation: Used as`isa swizzling`Technology for automatic key value observation. As the name implies, the ISA pointer points to the object class that maintains the dispatch table. The dispatch table essentially contains Pointers to methods implemented by the class, as well as other data. When an observer registers an object's properties, the isa pointer to the observed object is modified to point to an intermediate class instead of the real class. Therefore, the value of the ISA pointer does not necessarily reflect the actual class of the instance. You must never rely on isa Pointers to determine class membership. Instead, you should useclassMethod to determine the class of an object instance.Copy the code
validationisaKnow if it is actually modified:

Check the isa pointing to self.person after adding the registration:

As it turns out, self.person’s ISA points to NSKVONotifying_SSJPerson instead of SSJPerson after the addObserver step is registered.

The middle classNSKVONotifying_SSJPersonWith the originalSSJPersonWhat does class matter?

We know that isKindOfClass can determine whether an object belongs to a class or a subclass of that class. After printing above, we see that NSKVONotifying_SSJPerson is a subclass of SSJPerson.

Will “isa” be redirected to “SSJPerson” after removeObserver? Is the original NSKVONotifying_SSJPerson already destroyed?

Continue console printing:

This provides a method to get all subclasses of a run-time specified class:

// Gets a subclass of the specified class
+ (NSArray *)findSubClass:(Class)defaultClass
{
    // Total number of registered classes
    int count = objc_getClassList(NULL,0);
    
    // Create an array containing the given object
    NSMutableArray * array = [NSMutableArray arrayWithObject:defaultClass];
    
    // Get all registered classes
    Class *classes = (Class *)malloc(sizeof(Class) * count);
    objc_getClassList(classes, count);
    / / traverse
    for (int i = 0; i < count; i++) {
        if (defaultClass == class_getSuperclass(classes[i])) {
            [array addObject:classes[i]];
        }
    }
    free(classes);
    return array;
}
Copy the code

Remove in dealloc to look at the mode:

Intermediate classes generated by the addObserver step will not be destroyed when the observation mode is removed.

We found that
  • After addObserver: forKeyPath: options: context: this step will make isa observed object pointing in the direction of a dynamically generated in the middle of the class (inherited from the original class);

  • After removeObserver: forKeyPath: context: this step, will let the observed object isa point to the original class, and the middle class will not be destroyed.

The intermediate class is an existing class or a dynamically generated class.

Continue printing:

Explain NSKVONotifying_SSJPerson class is in addObserver: forKeyPath: options: context: after the generated.

What attributes does the NSKVONotifying_SSJPerson class have
// Iterate over all attributes
- (NSArray *)getAllProperties:(id)instanceOjb{
    u_int count = 0;
    // Pass the address of count
    objc_property_t *properties = class_copyPropertyList([instanceOjb class], &count);
    NSMutableArray *propertyArray = [NSMutableArray arrayWithCapacity:count];
    for (int i = 0; i < count; i++) {
        // The propertyName obtained is a C language string
        const char *propertyName = property_getName(properties[i]);
        [propertyArray addObject:[NSString stringWithUTF8String:propertyName]];
// NSLog(@"%@",[NSString stringWithUTF8String:propertyName]);
    }
    free(properties);
    return propertyArray;
}
Copy the code

Print the properties of the SSJPerson instance object:

The printed NSKVONotifying_SSJPerson instance object property:

What methods does NSKVONotifying_SSJPerson have

This provides a function that prints all instance methods of the class:

// Iterate over all the methods
// Iterate over all the methods
- (NSArray *)gainMethodList:(id)instanceOjb
{
    unsigned int methodCount = 0;
    Method *methodList = class_copyMethodList([instanceOjb class], &methodCount);
    NSMutableArray *methodArray = [NSMutableArray arrayWithCapacity:methodCount];
    for (int i = 0; i < methodCount; i++) {
        Method temp = methodList[i];
        SEL name_A = method_getName(temp);
        const char *name_sel = sel_getName(name_A);
        [methodArray addObject:[NSString stringWithUTF8String:name_sel]];
    }
    free(methodList);
    return methodArray;
}
Copy the code

Here to

printSSJPersonClass instance methods:

Print the NSKVONotifying_SSJPerson instance method

Does class_copyMethodList get its own instance method, or does it inherit from a parent class?

As you can see from the API’s comments, “class_copyMethodList” returns its own implementation. The subclass NSKVONotifying_SSJPerson overrides the parent SSJPerson instance method.

What does the setter method of the intermediate class do

So far, we know that after registering the observe mode, the isa of the observed object points to a new intermediate class (a subclass of the original class) that overrides the setter method of the original class. So what does this setter do?

Set observation mode for properties and member variables:

Running results:

Thus, the observation pattern works only for attributes, not for member variables.

Next, use LLDB symbolic breakpoints to see where the underlying function is going:

As you can see from the stack, the assignment of name on line 103 makes a series of function calls:

  1. _NSSetObjectValueAndNotify

  2. _changeValueForKey:key:key:usingBlock:

  3. _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:

  4. setName:

Note that the assignment to name is actually an assignment to the intermediate class, because by then ISA no longer refers to the original class, and the setName: method of the original class will eventually be called once through the series of functions.

The notification callback observeValueForKeyPath: ofObject: change: context: when was launched? The breakpoint:

We found that it was launched after NSKeyValueDidChange.

Let’s review the whole process of KVO

Part of the code

- (void)viewDidLoad {
    ...
    // Create an instance variable for SSJPerson
    self.person = [SSJPerson new];
    // Register the observation mode
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:SSJPersonNameContext];
    / / assignment
    self.person.name = @"Wang Dada";
}

// Observe the mode callback method
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    /** Business code */
}

- (void)dealloc{
    ...
    [self.person removeObserver:self forKeyPath:@"name" context:SSJPersonBFriendsNameArrayContext];
}
Copy the code
  1. calladdObserver:forKeyPath:options:context:Method,
  • The bottom layer dynamically generates the “NSKVONotifying_SSJPerson” class (which isa subclass of SSJPerson), and the person isa points to “NSKVONotifying_SSJPerson”,

  • NSKVONotifying_SSJPerson overrides instance methods of the “SSJPerson” class (such as setters, getters, etc.);

  1. Assigning the attribute value name (essentially the name of NSKVONotifying_SSJPerson) calls the following function
  • _NSSetObjectValueAndNotify

  • _changeValueForKey:key:key:usingBlock:

  • _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:

(here is called NSKeyValueDidChange, then launch “observeValueForKeyPath: ofObject: change: context:” callback)

  • setName:(This step will assign SSJPerson’s name)
  1. callremoveObserver:forKeyPath:context:Remove observation mode
  • Person’s ISA is redirected to SSJPerson

  • NSKVONotifying_SSJPerson will not be destroyed and will remain in memory