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. It is a derivative of the observer model. The basic idea is to add observation to an attribute of the target object. When the attribute changes, the KVO interface method implemented by the observer object is triggered to automatically notify the observer.

What is the observer mode? A target object manages all observer objects that depend on it and actively notifies them when its own state changes. This active notification is usually implemented by calling the interface methods provided by each observer object. The observer pattern perfectly decouples the target object from the observer object.

In simple terms, KVO can listen for changes in value by listening for key changes, which can be used to listen for state changes between objects. KVO is defined as an extension of NSObject. There is an explicit NSKeyValueObserving class name in Objective-C, so KVO can be used for any type that inherits NSObject. (Some pure Swift classes and constructs do not support KVO. Because you didn’t inherit NSObject.

KVO low-level exploration

#import "ViewController.h"
#import "MJPerson.h"

@interface ViewController(a)
@property (strong.nonatomic) MJPerson *person1;
@property (strong.nonatomic) MJPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    // Add a KVO listener to the person1 object
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@ "123"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.person1 setAge:21];
    
    [self.person2 setAge:22];
}

- (void)dealloc {
		// Don't forget to remove the listener, otherwise it will cause memory leaks
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

// is called when the value of the listening object's property changes
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context
{
    NSLog(@" listen to %@ change %@ - %@ - %@, object, keyPath, change, context);
}

@end
Copy the code

Result output:

Listen to the < MJPerson:0x6000039843f0> age property value changed - {kind =1;
    new = 21;
    old = 1;
} - 123
Copy the code

Why does person1 send notification of a property value change, but person2 does not? What happens when you add a KVO listener to Person1?

As we all know, when the instance object calls the object method, it first finds its own class object through the ISA pointer, and then searches in the list of object methods of the class object. If it cannot find it, it searches in the parent class through supperClass. So since the same method is executed, the result is not the same, then we can guess. The isa pointer to person1 and person2 must point to two different classes, which can be checked by the LLDB directive:

(lldb) p _person1.isa
(Class) $1 = NSKVONotifying_MJPerson
  Fix-it applied, fixed expression was: 
    _person1->isa
(lldb) p _person2.isa
(Class) $2 = MJPerson
  Fix-it applied, fixed expression was: 
    _person2->isa
Copy the code

The isa pointer for person1 points to NSKVONotifying_MJPerson and the ISA pointer for person2 points to MJPerson.

NSLog(@"person before adding KVO - person1: %@, person2: %@",object_getClass(self.person1), object_getClass(self.person2));
    
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
NSLog(@"person before adding KVO - person1: %@, person2: %@",object_getClass(self.person1), object_getClass(self.person2));
Copy the code

Result output:

Person1: MJPerson, person2: MJPerson Person1: NSKVONotifying_MJPerson, person2: MJPersonCopy the code

The isa pointer for person1 is still pointing to MJPerson until KVO is added to person1, but after KVO listener is added, it is changed to point to NSKVONotifying_MJPerson.

Let’s add some code to see where NSKVONotifying_MJPerson comes from:

Class person1Before = object_getClass(self.person1);

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

Class person1After = object_getClass(self.person1);
Copy the code

Enter the LLDB command:

(lldb) p person1Before.superclass
(Class) $0 = NSObject
(lldb) p person1After.superclass
(Class) $1 = MJPerson
Copy the code

The superclass of NSKVONotifying_MJPerson is MJPerson. A subclass of MJPerson that is dynamically generated by the Runtime and points the ISA pointer to Person1. NSKVONotifying_MJPerson overwrites the setAge method and adds KVO listener code to it.

NSLog(@"person before adding KVO - person1: %p, person2: %p \n"[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];

NSLog(@"person after adding KVO - person1: %p, person2: %p \n"[self.person1 methodForSelector:@selector(setAge:)], [self.person2 methodForSelector:@selector(setAge:)]);
Copy the code

Result output:

Person1 Before adding KVO listener -0x101F29650 0x101F29650 Person1 after adding KVO listener -0x7FFF257223da 0x101F29650Copy the code

– (IMP)methodForSelector:(SEL)aSelector is used to get the address of the implementation of person1 and person2. The implementation of person1’s setAge method has changed since the addition.

Next we use the LLDB directive to look at the name of the method implementation

(lldb) p (IMP)0x101f29650
(IMP) $0 = 0x0000000101f29650 (Interview01`-[MJPerson setAge:] at MJPerson.m:12)
(lldb) p (IMP)0x7fff257223da
(IMP) $1 = 0x00007fff257223da (Foundation`_NSSetIntValueAndNotify)
Copy the code

The p (IMP) method address can be used to print the method name, so person1’s setAge: method calls the Foundation framework’s _NSSetIntValueAndNotify() implementation after adding the KVO listener.

If the definition of the attribute is type is double is called _NSSetDoubleValueAndNotify ()

You can test it out for yourself. This method has a counterpart in the Foundtion framework

NSSetDoubleValueAndNotify()

NSSetCharValueAndNotify()

.

How to implement the _NSSetIntValueAndNotify() method, which involves the reverse of the Foundation framework, is not the focus of this section. If you are interested, you can find the relevant content by yourself. Here I will only make a conclusion:

Void _NSSetIntValueAndNotify() {[self willChangeValueForKey:@"age"]; [super setAge:age]; [self didChangeValueForKey:@"age"]; }Copy the code

So how do we test this statement?

@implementation MJPerson

- (void)setAge:(int)age{
    _age = age;
}

- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}

- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end");
}
@end
Copy the code

Result output:

WillChangeValueForKey didChangeValueForKey -begin <MJPerson:0x6000007e45c0> age property value changed - {kind =1;
    new = 21;
    old = 1;
} - 123
didChangeValueForKey - end
Copy the code

So we can outline the internal implementation of _NSSetIntValueAndNotify:

  • Call willChangeValueForKey:
  • Call the original setter implementation
  • Call didChangeValueForKey:
    • didChangeValueForKey:Internally it callsThe observer observeValueForKeyPath: ofObject: change: context:methods

NSKVONotifying_MJPerson override method

Rewrite the setter

In the setter, calls to the following two methods are added.

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
Copy the code

Then in didChangeValueForKey:, call:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey.id> *)change
                       context:(nullable void *)context;
Copy the code

Because the principle of KVO is to modify setter methods, you must call setters with KVO, and accessing property objects directly has no effect.

Rewrite the class

- (Class)class
{
    return [MJPerson class];
}
Copy the code

Masking the internal implementation hides the existence of the NSKVONotifying_MJPerson class

Override dealloc

The system overrides the dealloc method to release resources.

Rewrite _isKVOA

This private method is used to indicate that the class is a class generated by the KVO mechanism.

The interview questions

1. What is the nature of KVO?

KVO is implemented through ISa-Swizzling.

The essence of KVO is that the compiler automatically creates a derived class for the observed object and points the observed object’s ISA to that derived class. A derived class is a subclass of the original class of the object being observed, and if a user registers an observation on a property of the target object, the derived class overrides the setter method for that property and adds notification code to it.

2. How do I manually trigger KVO?

The implementation of KVO is to register two functions automatically implemented in keyPath, in setters, automatically called.

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
Copy the code

So if you want to do KVO manually, first, you need to manually implement setter methods for the property and call willChangeValueForKey: and didChangeValueForKey: before and after the set operation. Method used to notify the system that the value of the key attribute is about to change and has changed; Second, be automaticallyNotifiesObserversForKey implementation class methods, and in the setting of the key does not automatically send a notification (return). Note that other keys that are not implemented manually are passed to super for handling.

- (void) setAge:(int)theAge
{
    [self willChangeValueForKey:@"age"];
    age = theAge;
    [self didChangeValueForKey:@"age"];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
Copy the code

If you need to disable these KVO automaticallyNotifiesObserversForKey directly return NO, realize attribute setter method, don’t make calls willChangeValueForKey: And didChangeValueForKey: methods.

KVC

Key-value coding (KVC) refers to iOS development that allows developers to access attributes of objects directly through Key names or assign values to attributes of objects. Without calling an explicit access method. This allows you to access and modify the properties of an object dynamically at run time. Not at compile time, which is one of the dark arts of iOS development. Many advanced iOS development techniques are implemented based on KVC.

Common apis:

- (void)setValue:(id)value forKey:(NSString *)key;
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;

- (id)valueForKey:(NSString *)key; 
- (id)valueForKeyPath:(NSString *)keyPath;
Copy the code
  • KeyPath is the equivalent of finding properties by path, layer by layer,
  • Key is set by the name of the property directly, and an error will be reported if you look by path

SetValue: forKey: principle

  • First, check whether there is a corresponding setter method according to the order of setKey and _setKey. If there is, call it. Otherwise, go to the next step
  • The corresponding setter method was not found, so we’ll look at it next+ (BOOL)accessInstanceVariablesDirectlyDoes this method return YES, which it does by default?
    • If you override the method to return NO, KVC will call it at this stepsetValue:forUndefinedKey:Method and throws an NSUnknownKeyException.
    • Return YES, and KVC will follow_key._iskey.key.iskeyIf all member variables are not present, KVC will callsetValue:forUndefinedKey:Method and throws an NSUnknownKeyException.

Principle of valueForKey:

  • Check whether there are corresponding getter methods in the sequence of getKey, isKey, and _key. If there are getter methods, call them. Otherwise, go to the next step
  • The corresponding getter method was not found, we will look at it next+ (BOOL)accessInstanceVariablesDirectlyDoes this method return YES, which it does by default?
    • Return NO, then this step is called by KVCvalueForUndefinedKey:Method and throws an NSUnknownKeyException.
    • Return YES, and KVC will follow_key._iskey.key.iskeyIf all member variables do not exist, KVC will callvalueForUndefinedKey:Method and throws an NSUnknownKeyException.