KVO principle analysis

Posted by LuiePL on Sun, 02 Jan 2022 21:05:24 +0100

preface

*** KVO official document link

I Share some details about KVO

1.context function

First, let's take a look at the description of context

addObserver:forKeyPath:options:context: the context pointer in the message contains any data, which will be returned to the observer in the corresponding change notification. You can specify NULL and completely rely on the key path string to determine the source of change notification, but this method may cause problems for superclasses to observe objects with the same key path for different reasons.
A safer and more extensible approach is to use context to ensure that the notifications you receive are sent to your observers rather than superclasses.
The address of a uniquely named static variable in a class is a good context. Contexts selected in a similar manner in a superclass or subclass are unlikely to overlap. You can select a context for the entire class and rely on the critical path string in the notification message to determine what has changed. Alternatively, you can create a different context for each observed key path, which completely bypasses the need for string comparison and improves the efficiency of notification parsing. Listing 1 shows the example context of the balance and interestRate attributes selected in this way.

The example code is as follows

    self.person  = [LGPerson new];
    self.student = [LGStudent new];
    //If monitoring multiple familiar
    //Multi object key value observation
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:@"personNick"];
    
    [self.student addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:@"studentNick"];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    // Performance + code readability
    //context is equivalent to identifier judgment
    if (context  == @"personNick") {
        NSLog(@"%@",change);
    } else if (context  == @"studentNick") {
        NSLog(@"%@",change);
    }
    NSLog(@"%@",change);
}

Usage scenario: if there are multiple attributes to be monitored, they are multi-layer nested relationships, such as familiarity with the parent of the parent class, or duplicate names of different object attributes. If we judge the callback method, the logic of the code will be very cumbersome. Using context as a flag can be directly distinguished, which makes the code more readable and improves the performance

2. Is the observer removed

As we all know, we usually remove observers in dealloc method, but why do we remove them? Let's see the explanation in the relevant part of 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 memory.
  • The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer's initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.
    When dealloc is called, the observer will not automatically delete itself. When the observer continues to send notifications, it may send messages to the released observers, resulting in memory access exceptions. Therefore, it is necessary to ensure that the observer is removed when the observer is destroyed.
    The protocol does not provide a method to ask whether the object is an observer or an observer. A typical pattern is to register as an observer during observer initialization (such as init or viewDidLoad) and unregister during release (usually dealloc), to ensure correct pairing and orderly addition and deletion of messages, and the observer is unregistered before being released from memory

Therefore, it is necessary to cancel the registration before the observer is released, so it is most appropriate to cancel the registration in dealloc

3.KVO manual monitoring and automatic monitoring

Of course, automatic monitoring is used by default, but there may be some scenarios that require manual monitoring, such as setting some switches. In some fixed cases, observe some attributes, and in other times observe all attributes. At this time, you can use two writing methods to switch according to the switches

//Manual
- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    //Automatic or manual monitoring can be distinguished according to different key values
    //if(@ "switch"){
    	return NO;
    }
    return YES;
}

4. Observe attributes affected by multiple factors

What is an attribute affected by multiple factors? Take a common example
Download: generally, what we need to know is the download progress, and the download progress = downloaded / total. If the number of downloads and the total number are constantly changing, what should we do to observe the download progress? Look at the example below

Listen to downloadProgress and update the total number of downloads and by clicking

	self.person  = [LGPerson new];
    [self.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);
}

Associate attributes. By judging the key value, as long as totalData and writendata change, they will listen for the change of downloadProgress, and then rewrite the getter method to write out the operation relationship between the three

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}

Print the results by clicking: because the writendata changes after clicking totalData once, downloadprogress is printed twice

2021-07-29 12:24:29.374538+0800 001---KVO Probe into[1343:92197] {
    kind = 1;
    new = "0.198020";
}
2021-07-29 12:24:29.374701+0800 001---KVO Probe into[1343:92197] {
    kind = 1;
    new = "0.196078";
}

5. Observation of variable arrays

The use of KVO to monitor the changes of collection type data depends on the understanding of KVC. Of course, the official documents also explain it. I won't explain it one by one here. You can go there KVO official document link Look, the code is as follows

    self.person = [DMPerson new];
    self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"xinge"];

Print results

2021-07-29 12:32:14.290173+0800 001---KVO Probe into[1384:98856] {
    indexes = "<_NSCachedIndexSet: 0x6000009d55c0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        xinge
    );
}

II KVO principle analysis

In fact, you may know the principle of KVO more or less, because these are often encountered in interviews, but have you really verified it? Let's go and verify it today

1. Dynamically generate derived class NSKVONotifying_xxx

We verify through LLDB debugging that we break the point before adding an observer

After moving to the next step, nskvonotifying is generated dynamically during printing_ LGPerson

Of course, just in case, I can also see if it is generated during compilation. We can see nskvonotifying before adding viewers_ Whether lgperson exists or not

Through this method, you can traverse the current class and all subclasses

- (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);
}

Print before and after

[self printClasses:[LGPerson class]];
self.person = [[LGPerson alloc] init];
[self printClasses:[LGPerson class]];

The printing results are as follows

2021-07-29 12:52:02.222312+0800 002---KVO Principle discussion[1590:113159] classes = (
    LGPerson,
    LGStudent
)
2021-07-29 12:52:02.224991+0800 002---KVO Principle discussion[1590:113159] classes = (
    LGPerson,
    "NSKVONotifying_LGPerson",
    LGStudent
)

It can be seen that nskvonotifying is generated dynamically when adding observers_ Lgperson is a subclass. Next, let's look at what the subclass stores. According to the previous structural analysis of the class, we all know that the class stores nothing but member variables, methods, protocols and other information. Then we can traverse the class through the following methods

#pragma mark * * - traversal method Ivar property**
- (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);
}

Print kvonifynotifying separately_ LGPerson and LGPerson, the results are as follows

2021-07-29 19:25:56.595778+0800 002---KVO Principle discussion[1058:62119] Get derived class method
2021-07-29 19:25:56.596011+0800 002---KVO Principle discussion[1058:62119] setNickName:-0x10c7b503f
2021-07-29 19:25:56.596185+0800 002---KVO Principle discussion[1058:62119] class-0x10c7b3b49
2021-07-29 19:25:56.596282+0800 002---KVO Principle discussion[1058:62119] dealloc-0x10c7b38f7
2021-07-29 19:25:56.596380+0800 002---KVO Principle discussion[1058:62119] _isKVOA-0x10c7b38ef
2021-07-29 19:25:56.596499+0800 002---KVO Principle discussion[1058:62119] Gets the current class method
2021-07-29 19:25:56.596609+0800 002---KVO Principle discussion[1058:62119] setNickName:-0x10c4a44c0

It can be found that the subclass actually overrides these methods of the parent class_ isKVOA is used to judge whether this class is a dynamically generated derived class

2. Will the derived class be destroyed after the observer is removed

First, look at dealloc. As we said before, when we don't need an observer, we need to remove the observer, otherwise it may cause memory access exceptions. At the same time, we know that adding an observer actually changes the direction of ISA and points to the newly generated derived class. When we remove the observer, let's see whether isa refers back to the original class


It can be found that after removing the observer in dealloc, isa does refer back to the original class, so the derived class nskvonotifying_ Will lgperson be destroyed? Let's further verify. The verification is also very simple. After dealloc, we print nskvonotifying in LLDB_ Lgperson can be checked to see if it exists


We print the derived class nskvonotifying before leaving the current class and re adding the observer_ If lgperson is found to exist, it means that the derived class has a cache. The next time you add an observer, you don't have to recreate it. That means that nskvonotifying is also called here_ The dealloc method rewritten by lgperson realizes the redirection of isa in the rewritten dealloc method

3. Analyze the method of derived class override

Now that a new subclass has been re created, let's take a look at the differences of this class and see what the overridden class methods do. dealloc has said. Next, let's take a look at the setter method. First, add a member variable name to LGPerson


Why do you do this? Because only attributes generate setter s, getter s, and member variables. If there are no, then add observers to them to listen


It can be found that the value of the member variable is changed without monitoring. Does this prove that the so-called observation actually monitors the call of the setter method? Does this also explain why the setter method is rewritten when KVO monitors manually? Next, let's do some tests after dealloc removes the observer


It can be found that after removing the observer, isa has logically pointed back to LGPerson, and the setter method previously called should be the derived class nskvonotifying called_ The setter method of LGPerson. Why has the property of LGPerson changed now? Does it explain that the derived class nskvonotifying_ The setter method of LGPerson actually assigns a value to the attribute of the parent class. Of course, it still needs to be verified. How to verify it? We just need to get the stack information when calling the setter method


Obtain the call stack of setter method interval through the lower symbolic breakpoint, and directly enter the assembly by triggering the call, so as to obtain the underlying stack information


From the stack information, it can be found that after calling touchesBegan:withEvent:, the setter method of LGPerson is re executed through a series of calls. Because #2-#4 the steps are all Foundation library methods, they are not open source. Of course, the class method will have similar operations, and we can also try them briefly


Similarly, call the class method before and after adding an observer, and the print results are exactly the same, indicating the derived class NSKVONotifying_LGPerson also carried out a series of operations in the process of calling the class method, and finally called back to the class method of the parent class, so as to hide the intermediate execution process and form a closed loop. I won't debug this at the next symbolic breakpoint. Today's exploration is limited for the time being

4. Summary of KVO process

  • 1. After adding an observer, a derived class named nskvonotifying will be automatically generated_ XXX, and point the isa of the instance object to the derived class, which is a subclass of the observed object

  • 2.NSKVONotifying_xxx rewrites the class, dealloc and setter methods of the parent class in order to hide the real execution process of the underlying method, form a closed loop, and generate a_ The identification method of isKOVA is used to judge whether it is a derived class

  • 3. Observation of object properties is actually a call to the setter method that listens for object properties (which perfectly fits why Manual listening rewrites the setter method)

  • 4. Removing an observer is not a real removal. It just re refers isa to the original object and saves the derived class to avoid re creation when adding an observer next time

That's all for today's sharing. The next chapter Custom KVO

Topics: iOS xcode objective-c cocoa cocoapods