Basic principles of iOS -KVO

[TOC]

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

How do I trigger KVO manually

1. Conceptual understanding

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:self forKeyPath:@"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(@" monitor %@ change %@", 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.

Second, 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 p1 calls the same set method as P2, even if we override the set method, but we find that P1 also executes listeners in addition to calling setobserveValueForKeyPathMethods.

Note that KVO has made some changes to the P1 object at runtime. So p1 might have done something extra when setage was called. So the problem is that the objects are different in memory. They might be different in nature. Let’s explore how this is done inside KVO.

Iii. Analysis of KVO underlying implementation

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 class object through the ISA pointer in the P2 object, and then finds the Setage method in the class 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. Therefore, when p1 object calls setAge method, it must find NSKVONotifyin_Person according to P1 ISA, and find the method and implementation of setAge in NSKVONotifyin_Person.

After consulting the data, we can learn. The setAge method in NSKVONotifyin_Person calls the Fundation C function _NSsetIntValueAndNotify.

  • 1. First call willChangeValueForKey to change the method
  • 2. Then call the parent classsetAgeMethod assigns a value to a member variable
  • 3. The last call to didChangeValueForKey has changed the method. The listener’s listener method is called in didChangeValueForKey, which ends up in the listener’s observeValueForKeyPath method.

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 the method to see how the address of p1 and P2 setage method changes before and after adding KVO.

NSLog(@" before adding KVO listener -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

What is the internal structure of NSKVONotifyin_Person?

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:self forKeyPath:@"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

Return class_getSuperclass(object_getClass(self)); }Copy the code

Seven, 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: context: method.

Viii. Answer questions:

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

    • A. When an object uses KVO listening, iOS changes the isa pointer to the object to point to a new subclass that is dynamically created by the Runtime and has its own set implementation that is called sequentially internallywillChangeValueForKeyMethod, the originalsetterMethod implementation,didChangeValueForKeyMethod, anddidChangeValueForKeyThe listener will be called inside the method againobserveValueForKeyPath:ofObject:change:context:Listening methods.
  • How do I trigger KVO manually

    • A. KVO is automatically triggered when the value of the monitored property is changed. If we want to trigger KVO manually, we need to call it 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:self forKeyPath:@"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.