How does iOS implement KVO for an object? (What is the nature of KVO?) How do I trigger KVO manually

Firstly, we need to know the basic usage of KVO. The full name of KVO is key-value Observing, commonly known as “key-value monitoring”, which can be used to monitor the change of an object’s attribute Value.

- (void)viewDidLoad { [super viewDidLoad]; Person *p1 = [[Person alloc] init]; Person *p2 = [[Person alloc] init]; p1.age = 1; p1.age = 2; p2.age = 2; / / the self monitoring the age attribute of the p1 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:selfforKeyPath:@"age" options:options context:nil];
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"%@ listening to %@ changed %@", object, keyPath,change); } Person: 0x604000205460> {kind = 1; new = 10; old = 2; }Copy the code

As you can see from the code above, after adding a listener, the value of the age property is notified to the listener when it changes, executing the listener’s observeValueForKeyPath method.

1. Explore the underlying implementation principle of KVO

We can go to the Person class and override the set method of age to see if KVO has done something inside the set method to notify the listener.

We find that even if we override the set method, p1 and P2 call the same set method, but we find that P1 also executes the observeValueForKeyPath method of the listener in addition to calling the set method.

Note that KVO has made some changes to the P1 object at runtime. So p1 might have done something extra when it called setAge:, so there’s a problem with the object. The two objects must be different in memory, and they might not be the same in nature. Let’s explore how this is done inside KVO.

Two: KVO underlying implementation analysis

First, let’s look at the interrupt point where we added the listener in the code above. What does the addObserver method do to the p1 object? That is, what happens to the P1 object after it passes through the addObserver method by printing the ISA pointer as shown below

According to the figure above, after the addObserver operation is performed on p1 object, the ISA pointer of P1 object changes from Person to NSKVONotifyin_Person, while p2 object does not change at all. That is, once the KVO listener is added to p1, the ISA pointer will change, so the set method will not perform the same.

So let’s first look at how p2 objects are stored in the content, and then compare p1 to P2. First of all, when P2 calls setAge:, it finds the Person object through the ISA pointer in the P2 object, and then finds the setAge: method in the p2 object. Then find the corresponding implementation of the method. As shown in the figure below

NSKVONotifyin_Person isa subclass of Person. NSKVONotifyin_Person is generated by the Runtime at runtime. So when p1 calls setAge:, it must find NSKVONotifyin_Person according to P1 isa, and find setAge: method and implementation in NSKVONotifyin_Person.

After consulting the data, we can learn. The setAge method in NSKVONotifyin_Person actually calls the Fundation C function _NSsetIntValueAndNotify, and what it does internally is say, First call willChangeValueForKey will change the method, then call the setAge method of the parent class to assign a value to a member variable, and finally call didChangeValueForKey to change the method. The listener’s listener method is called in didChangeValueForKey, which ends up in the listener’s observeValueForKeyPath method.

Three: how to verify that KVO is really implemented in the way mentioned above?

We have already verified that the isa pointer points to a subclass of Person created by Runtime, NSKVONotifyin_Person, when executing the listener. In addition, we can print the address of p1 and P2 setAge: before and after adding KVO

// Use methodForSelector to find the method implementation's address, NSLog(@)"-p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
    
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"After adding KVO listener -p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
Copy the code

We found that before KVO listening was added, the address of p1 and P2’s setAge method was the same, but after KVO listening, the address of P1’s setAge method was changed. We used the printing method to realize the change, and found that it was exactly the same as what we said above. The implementation of THE setAge method of P1 is transformed by the setAge method of the Person class method into the _NSsetIntValueAndNotify function of the Foundation framework of C language.

The Foundation framework calls different methods depending on the type of property. For example, with the age property of type int we defined earlier, we see the _NSsetIntValueAndNotify function called in the Foundation framework. So let’s change the property type of age to double and reprint it

We found that the function called into _NSSetDoubleValueAndNotify, so this suggests that the Foundation framework has a lot of this type of function, through the different types of different function called attributes. So we can assume that there are many other Foundation frameworks such as _NSSetBoolValueAndNotify, _NSSetCharValueAndNotify, _NSSetFloatValueAndNotify, and _NSSetLongValueAn DNotify and so on. You can go to the Foundation framework file and query the keywords on the command line to find relevant functions

Four: NSKVONotifyin_Person internal structure is what?

NSKVONotifyin_Person is a subclass of Person whose superclass pointer points to the Person class, and NSKVONotifyin_Person must have a separate implementation of the setAge method. The difference between NSKVONotifyin_Person and the Person class may lie in the object methods and implementation of NSKVONotifyin_Person. We print the Person object and the object methods stored in the NSKVONotifyin_Person object via Runtime

- (void)viewDidLoad { [super viewDidLoad]; Person *p1 = [[Person alloc] init]; P1. The age = 1.0; Person *p2 = [[Person alloc] init]; P1. The age = 2.0; / / the self monitoring the age attribute of the p1 NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:selfforKeyPath:@"age" options:options context:nil];

    [self printMethods: object_getClass(p2)];
    [self printMethods: object_getClass(p1)];

    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void) printMethods:(Class)cls
{
    unsigned int count ;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0 ; i < count; i++) {
        Method method = methods[i];
        NSString *methodName  = NSStringFromSelector(method_getName(method));
        
        [methodNames appendString: methodName];
        [methodNames appendString:@""];
        
    }
    
    NSLog(@"% @",methodNames);
    free(methods);
}
Copy the code

The print content is as follows

Using the above code we see that there are four object methods in NSKVONotifyin_Person. SetAge:, class, dealloc, _isKVOA, so we can draw the memory structure and method call order of NSKVONotifyin_Person.

Here NSKVONotifyin_Person overrides the class method to hide NSKVONotifyin_Person. Hidden from the outside world. After adding the KVO listener to p1, we can print the class of p1 and P2 objects and see that they both return Person.

NSLog(@"% @, % @",[p1 class],[p2 class]); // Print the result Person,PersonCopy the code

If NSKVONotifyin_Person doesn’t override the class method, then when an object calls a class object method, it just goes all the way up to NSObject, and NSObject’s class implementation basically returns the class that its ISA points to, Return the class that ISA points to in P1 and the printed class is NSKVONotifyin_Person, but Apple doesn’t want to expose the NSKVONotifyin_Person class and doesn’t want us to know the internal implementation of NSKVONotifyin_Person, So I’ve internally overridden the class class to return the Person class directly, so when I call p1’s class object method, it’s the Person class. So p1 gives the impression that p1 is still a Person class and doesn’t know that the NSKVONotifyin_Person subclass exists.

So we can guess that the internal implementation of the class overwritten in NSKVONotifyin_Person is roughly

- (Class) Class {// Get the Class object, find the parent Class of the Class objectreturn class_getSuperclass(object_getClass(self));
}
Copy the code

5: verify didChangeValueForKey: internal calls the observer observeValueForKeyPath: ofObject: change: context: method

We simulate their implementation by overriding the willChangeValueForKey and didChangeValueForKey methods in the Person class.

- (void)setAge:(int)age
{
    NSLog(@"setAge:");
    _age = age;
}
- (void)willChangeValueForKey:(NSString *)key
{
    NSLog(@"willChangeValueForKey: - begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey: - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey: - end");
}
Copy the code

Run again to see what’s running inside the didChangeValueForKey method, as you can see by printing, Indeed inside didChangeValueForKey method has been called the observer observeValueForKeyPath: ofObject: change: the context method.

Answer the questions:

  • How does iOS implement KVO for an object? (What is the nature of KVO?)

** When an object uses a KVO listener, iOS will modify the isa pointer to point to a new Runtime subclass that has its own set implementation. The set implementation internally calls the willChangeValueForKey method, the original setter implementation, didChangeValueForKey method, And internal didChangeValueForKey method will be called the listener observeValueForKeyPath: ofObject: change: the context to monitor method. **

  • How do I trigger KVO manually

KVO is automatically triggered when the value of the monitored property is modified. If you want to trigger manuallyKVO, we need to call ourselveswillChangeValueForKeyanddidChangeValueForKeyMethod can trigger KVO manually without changing the property value, and both methods are necessary.

This can be verified by the following code

Person *p1 = [[Person alloc] init]; P1. The age = 1.0; NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [p1 addObserver:selfforKeyPath:@"age" options:options context:nil];
    
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
    
[p1 removeObserver:self forKeyPath:@"age"];
Copy the code

Through the print we can find that didChangeValueForKey within a method call the observeValueForKeyPath success: ofObject: change: context:, and the value of the age did not change.