What is KVO?

KVO stands for KeyValueObserving, commonly known as key value monitoring. Allows you to listen for changes in object properties in response to specific events.

What did KVO do?

Override setter methods on objects? I’ve overridden the setter for age in my Person class,

@interface Person : NSObject

@property (nonatomic.assign) NSInteger age;

@end

#import "Person.h"

@implementation Person

- (void)setAge:(NSInteger)age {
    NSLog(@set age: %ld, age);
    _age = age;
}

@end
Copy the code

The observeValueForKeyPath method for discovering listeners is still executed

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *p1 = [Person new];
    p1.age = 1;
    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(@" Listen to %@ change %@", object, keyPath, change);
}
Copy the code

To think about whether you have made some changes to the objects, try interrupting to print their memory address: before the Person object addObserver:

(lldb) po p1
<Person: 0x600003924b70>
Copy the code

After the addObserver

(lldb) po p1
<Person: 0x600003924b70>
Copy the code

Nothing has changed. Try printing their ISA Pointers again

(lldb) po p1->isa
Person

(lldb) po p1->isa
NSKVONotifying_Person
Copy the code

The isa pointer to the Persion object has changed!

Underlying implementation analysis

Normally, a Person instance object, if you set its properties, finds the class object it belongs to through its ISA pointer, finds its setAge method through the class object, and then finds its implementation.

(lldb) po [p1->isa isSubclassOfClass:[Person class]]
YES
Copy the code

NSKVONotifying_Person is a subclass of the Person class, indicating that it is generated at runtime. So when p1 calls setAge, the class object it finds is already NSKVONotifying_Person, and the setAge method is called. The setAge method in NSKVONotifying_Person actually calls the Foundation C function _NSSetIntValueAndNotify. Nssetintvalueandnotify internally does the equivalent of calling willChangeValueForKey first, then calling the parent setAge method to assign a value to a member variable, and finally calling didChangeValueForKey. The listener method is called in didChangeValueForKey and ends up in the observeValueForKeyPath method that listens here.

Validate the above analysis

Let’s examine the memory address of the setAge method

NSLog(@"P1 setAge = %p", [p1 methodForSelector:@selector(setAge:)]);
    
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
NSLog(@"P1 setAge = %p", [p1 methodForSelector:@selector(setAge:)]);
Copy the code

Get the method implementation address using the methodForSelector method

2020-03-17 23:28:06.388425+0800 AwesomeOC[1599:156522] add KVO listener before P1setAge = 0x102d71390
(lldb) p (IMP)0x102d71390
(IMP) $0 = 0x0000000102d71390 (AwesomeOC`-[Person setAge:] at person.m :13) 2020-03-17 23:28:25.218130+0800 AwesomeOC[1599:156522setAge = 0x7fff257228bc
(lldb) p (IMP)0x7fff257228bc
(IMP) The $1 = 0x00007fff257228bc (Foundation`_NSSetLongLongValueAndNotify)
Copy the code

After the get method address, printing method, found that after adding KVO, realize from the Person class to achieve the Foundation framework of _NSSetLongLongValueAndNotify * * * *.

Note _NSSetLongLongValueAndNotify here, now I set the age type is NSInteger, if I change to a double type, then it becomes _NSSetDoubleValueAndNotify. Different functions are called based on different attributes. You can guess there’s also nsSetBoolValueAndNotify, NSSetFloatValueAndNotify, and so on.

NSKVONotifying_Person

We use the Runtime to print the instance methods stored in the Person and NSKVONotifying_Person objects, respectively.

- (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 result is as follows:

2020-03-17 23:40:29.327795+0800 AwesomeOC[1731:176338] add P1 before KVO listenersetAge = 0x10de67190 2020-03-17 23:40:29.327921+0800 AwesomeOC[1731:176338] person-agesetAge: 2020-03-17 23:40:29.328166+0800 AwesomeOC[1731:176338setAge = 0x7fff2572215c
2020-03-17 23:40:29.328254+0800 AwesomeOC[1731:176338] NSKVONotifying_Person - setAge: class dealloc _isKVOA
Copy the code

The Person class has the age and setAge methods, and the NSKVONotifying_Person class has four methods: setAge, class, dealloc, _isKVOA. Now we can rewrite the order of their calls.

- (Class)class {
	return class_getSuperclass(object_getClass(self));
}
Copy the code

WillChangeValueForKey and didChangeValueForKey

Override both methods in the Person class

- (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 it again to see what it prints

2020-03-17 23:56:56.052579+0800 AwesomeOC begin 2020-03-17 23:56:56.052661+0800 AwesomeOC[1813:197178] willChangeValueForKey End 2020-03-17 23:56:56.052721+0800 AwesomeOC[1813:197178] DidChangeValueForKey begin 2020-03-17 23:56:56.052906+0800 AwesomeOC[1813:197178] monitor <Person: 0x600002A30470 > age changes {kind = 1; new = 10; old = 2; } AwesomeOC[1813:197178] didChangeValueForKey endCopy the code

You can see from the order here that inside the didChangeValueForKey method, ObserveValueForKeyPath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary

*)change Context :(void *)context method.
,id>

So can we trigger KVO manually?

The answer is yes. This is done by manually calling the willChangeValueForKey and didChangeValueForKey methods.

[p1 willChangeValueForKey: @ "age"); [p1 didChangeValueForKey: @ "age");Copy the code

Print the result

AwesomeOC[68954:1537453] listen to <Person: 0x600002D80Fe0 > age change {kind = 1; new = 2; old = 2; }Copy the code

Now I have a pretty good idea of KVO

After KVO is added to the instance object, the Runtime creates an NSKVONotifying_XXX class that inherits it at runtime, and the isa pointer to the instance object points to that class. The class of the KVO instance object is NSKVONotifying_XXX. The reason why we print its class is that NSKVONotifying_XXX overwrites the class method. When we set the property, we’ll find the NSKVONotifying_XXX class method, which I guess is the function that sets the property there

- (void)setAge:(NSInteger)age {
	[self willChangeValueForKey];
	[super setAge:age];
	[self didChangeValueForKey];
}
Copy the code

Eventually, didChangeValueForKey calls back to observeValueForKeyPath.

Reference juejin. Cn/post / 684490…