Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.


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

How do I trigger KVO manually?

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.

Basic use of KVO listening. Example code:

Create a Person class

@interface Person : NSObject
@property(nonatomic, assign) int age;
@end


@implementation Person
@end
Copy the code

Use the Create Person object in the controller and change the value of the Person age property when you click on the screen.

#import "Person.h"
@interface ViewController ()
@property(nonatomic, strong) Person *person;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
    person.age = 9;
    self.person = person;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person.age = 10;
}

Copy the code

If we want to change the value of the person’s age property. Tell the controller to do something about it. So we’re going to find a listener, and we’re going to listen for changes in the age property of person. How to do that.

The usual practice is:

/ / add KVO listen to person objects NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:self forKeyPath:@"age" options:options context:nil];Copy the code

And write the callback method in the listener’s class:

// When the listener's property value changes, - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@" monitoring %@ property value changed -%@",object, keyPath, change); }Copy the code

Note: When not listening, destroy:

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"age"];
}
Copy the code

At this point, we add a new property to Person and have the controller listen for the new property. Such as the height. At that point, we can just increase the surveillance.

1. @property(nonatomic, assign) int height;

2. 

Person *person = [[Person alloc] init];
    person.age = 9;

    person.height = 11;

    self.person = person;
3.

 [self.person addObserver:self forKeyPath:@"height" options:options context:nil];


4.

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

{

    self.person.age = 10;

    self.person.height = 50;

}
Copy the code

At this point, we click on the screen and detect that KVO is called twice

// When the listener's property value changes, - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@" monitoring %@ property value changed -%@",object, keyPath, change); }Copy the code

Context :(void *)context is the nil argument that we passed earlier

[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
Copy the code

If we change the code:

[self.person addObserver:self forKeyPath:@"age" options:options context:@"333"];

[self.person addObserver:self forKeyPath:@"height" options:options context:@"444"];
Copy the code

When we listen to the context method the parameters will come in.

// When the listener's property value changes, - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@" monitoring the %@ property value of %@- %@-context:%@",object, keyPath, change, context); }Copy the code

So much for the basic usage of KVO, that’s it. Next, the KVO essence we create person2. And change the value of the person2 property when you click on the screen, but we don’t listen for person2 property changes.

Person *person2 = [[Person alloc] init];
    self.person2 = person2;
    self.person2.age = 15;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person.age = 10;
    self.person2.age = 10;
}
Copy the code

Rewrite the setAge, setHeight method of Person. Interrupt point detected by listening.

@implementation Person
- (void)setAge:(int)age

{
    NSLog(@"%d",age);
    _age = age;
}
@end
Copy the code

We see that the setAge: method is called for person2 Person when assigning the age property.

But the observeValueForKeyPath: listens for the callback when the Age property of the Person object changes. Since person2’s age property is not being listened to, observeValueForKeyPath is not called…

// When the listener's property value changes, - (void) observeforkeypath :(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@" monitoring %@ property value changed -%@",object, keyPath, change); }Copy the code

Why are we listening for changes to the Person property

No listening for changes to the person2 property?

We found that when they changed the age property, they did it by calling the setAge method.

But how did one pick up on KVO and the other

There’s no wiretap?

What is the essence?

The problem is not with the setAge method, because everyone calls the same method.

The problem can occur with the Person object.

I interrupt point to find: NSKVONotifying_Person (LLDB) p self.person.isa** (Class) $1 = NSKVONotifying_Person Fix it applied, fixed expression was: Self. Person ->isa person2 isa pointer to person:  (lldb) p self.person2.isa** (Class) $2 = Person Fix-it applied, fixed expression was: self.person2->isaCopy the code

In the previous article, we said that ISA refers to the class object of the instance object. We found that

The isa pointer to Person, Person2 points to different class objects.

Why is the class object different from the object created by the same class?

We can guess that we used KVO to listen for changes in the property of Person. It didn’t work

KVO listens for attribute changes for Person2.

Objects not monitored by KVO:

The ISA pointer to Person points to the Person class object.

The ISA for Person2 points to the NSKVONotifying_Person class object.

NSKVONotifying_Person is a subclass of Person that is dynamically created using the Runtime.

Pseudocode for the NSKVONotifying_Person class:

@interface NSKVONotifying_Person : Person

@end

Copy the code
@implementation NSKVONotifying_Person - (void)setAge:(int)age { _NSSetIntValueAndNotify(); } void _NSSetIntValueAndNotify() { [self willChangeValueForKey:@"age"]; [super setAge:age]; [self didChangeValueForKey:@"age"]; } - (void)didChangeValueForKey:(NSString *)key {// So-and-so attribute values changed [observer observeValueForKeyPath: key ofObject: self change: nil context: nil]; } @endCopy the code

This is why the person will receive the property value changes observeValueForKeyPath:… The call. Person2 does not receive property value changes.

Internally perform logical validation:

Write some code to verify that,

NSLog(@"person before adding KVO -%@,%@",object_getClass(self.person),object_getClass(self.person2)); NSLog(@"person before adding KVO -%@,%@",object_getClass(self.person2)); / / add KVO listen to person objects NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:self forKeyPath:@"age" options:options context:nil]; NSLog(@"person after adding KVO -%@,%@",object_getClass(self.person),object_getClass(self.person2));Copy the code
Nstance object Indicates the class object before adding KVO. Instance object The class object after KVO is added. * * 2018-06-09 17:35:18. 186795 + 0800 KVO principle of * * * * * * * * [2701-80467] person * * * * * * * * add KVO before * * * * * * * * - the person, the person * * * * 2018-06-09 17:35:21. 410571 + 0800 KVO principle of * * * * * * * * [2701-80467] person KVO add * * * * * * * * * * * * * * * * - NSKVONotifying_Person, after the person * * * * person After adding KVO listener, change from Person to **NSKVONotifying_Person**** ** test whether the implementation address of the setAge method has changed after adding KVO to Person. **Copy the code
NSLog(@"person before adding KVO -%p,%p",[self.person methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]); / / add KVO listen to person objects NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.person addObserver:self forKeyPath:@"age" options:options context:nil]; NSLog(@"person after adding KVO -%p,%p",[self.person methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);Copy the code
* * 2018-06-09 17:47:27. 560033 + 0800 KVO principle of * * * * * * * * [2937-100797] person * * * * * * * * add KVO before * * * * * * * * x10f95c570 0, 0 x10f95c570 * * * * 2018-06-09 17:47:29. 195660 + 0800 KVO principle of * * * * * * * * [2937-100797] person * * * * * * * * add KVO after * * * * * * * * x10fd01f8e 0, 0 x10f95c570 * * ** The address of the implementation has changed. But what exactly is an implementation? How do I see the implementation of a method? * * * * 2018-06-09 17:50:09. 292854 + 0800 KVO principle of * * * * * * * * [3027-107515] person * * * * * * * * add KVO before * * * * * * * * x1005b0570 0, 0 x1005b0570 * * * * 2018-06-09 17:50:11. 988789 + 0800 KVO principle of * * * * * * * * [3027-107515] person * * * * * * * * add KVO after * * * * * * * * x100955f8e 0, 0 x1005b0570 * * **(LLDB) p (IMP) 0x1005B0570 **(IMP) $0 = 0x00000001005b0570 (KVO mechanism '-[Person setAge:] at person.m :12) **(LLDB) p (IMP) 0x100955F8E ** (IMP) $1 = 0x0000000100955F8e (Foundation '_NSSetIntValueAndNotify) **\ ** It's _NSSetIntValueAndNotify in the Foundation module. Since the Foundation module is not open source, we do not currently know the source code. If you have a jailbroken cell phone. If you are experienced in hacking, you can check out the compiled files of Foundation. Using a decompiler tool, look at the assembly code.Copy the code
_NSSetIntValueAndNotify () does exist in the Foundation framework. ** ** Also, you will find: A _NSSetIntValueAndNotify will not be in * * * * * * * * and there will be _NSSetDoubleValueAndNotify \ _NSSetObjectValueAndNotify * * * * * * **_NSSetFloatValueAndNotify** ** and so on, depending on the type of your property. Different methods are called at the bottom. What about ** **_NSSet*ValueAndNotify's internal implementation? ** ** : ** **[self willChangeValueForkey:@"age"]; * * * * / / original setter realize * * * * * * * * [self didChangeValueForkey: @ "age"); DidChangeValueForkey * * * * * * * * * * * * internal invokes the observer observerValueForKeyPath: ofObject: \ * * * * change: context: method. **Copy the code
Can be rewritten in person - (void) willChangeValueForKey: (nsstrings *) key {[super willChangeValueForKey: key]; NSLog("willChangeValueForKey"); } - (void)didChangeValueForKey:(NSString *)key { NSLog("didChangeValueForKey- start"); / / * * observerValueForKeyPath internal calls the super didChangeValueForKey 】 【 : ofObject: \ * * * * change: context: method. ** [super didChangeValueForKey:key ]; NSLog("didChangeValueForKey - end"); } to test. The NSKVONotifying_Person class overrides setAge:, dealloc, and _isKVO.Copy the code
@implementation NSKVONotifying_Person - (void)setAge:(int)age { _NSSetIntValueAndNotify(); } // The internal implementation of the screen hides the presence of the NSKVONotifying_Person Class - (Class) Class {return [Person Class]; } - (void)dealloc {// end work} - (BOOL)_isKVOA {return YES; } @endCopy the code
NSKVONotifying_Person overrides setAge:, dealloc, and _isKVO. Use runtime, Runtime to verify.Copy the code
- (void)printMethodNamesOfClass:(Class)cls { unsigned int count; Method *methodList = class_copyMethodList(CLS, &count); // Store method name NSMutableString *methodNames = [NSMutableString String]; For (int I = 0; i < count; Method Method = methodList[I]; NSString *methodName = NSStringFromSelector(method_getName(method)); [methodNames appendString:methodName]; [methodNames appendString:@", "]; } // Free (methodList); // Prints the method name NSLog(@"%@ %@", CLS, methodNames); }Copy the code
[self printMethodNamesOfClass:object_getClass(self.person)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
Copy the code
**NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,** **Person setAge:, age,** How does iOS implement KVO for an object? (What is the nature of KVO?) Use the RuntimeAPI to dynamically generate a subclass and have the Instance object's ISA point to the new subclass. When modifying the properties of an instance object, Foundation's _NSSetXXXValueAndNotify function willChangeValueForKey is called. Internal will trigger the listener (Obsever) surveillance methods (obseverValueForKeyPath: ofObject: change: context:) how to manually trigger the KVO? KVO is usually triggered automatically, so I want to perform the triggering logic even if no one changes the attributes of the Person object, calling the following method directly. WillChangeValueForKey: didChangeValueForKey: Does directly modifying a member variable trigger KVO? KVO will not trigger.Copy the code