preface

In the daily development process, we often use KVO to carry out some development, so that our development is more simple and direct. So what exactly is the KVO? What is his underlying principle? We’re going to explore that today

concept

Concept description of KVO

KVO is an Objective-C implementation of the Observer Pattern. The observer object is notified when one of the properties of the observed object changes. Generally, objects that inherit from NSObject support KVO by default. KVO is the poster child for responsive programming.

The role of the KVO

  1. Listen for basic controls with state, such as switches, buttons, etc.
  2. Listen for string changes, and do some custom operations when the listening string changes;
  3. The view component can be dynamically updated when the data in the data model changes, for example, displaying the updated data in the data modeltableviewRefresh the list if data changes inscrollViewthecontentOffsetProperty to listen for a page slide.

Simply use KVO

Let’s look at an example. This is the simplest way to use KVO

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [DMPerson new];
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.nick = [NSString stringWithFormat:@ % @ "+".self.person.nick];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context{
    NSLog(@ "% @",change);
}

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

Run the code and simply click on the screenWe created oneDMPersonOf the instance object of itsnickProperties are observed when each timenickIs called when a new value is assignedobserveValueForKeyPathThe method tells us. At the same time, we aretouchesBeganMethod tonickAssignment, every time we touch the screen, we’re going to do an assignment.

The context of KVO

When we look at the example above, we can see that the addObserver method needs to pass several parameters

  • The first one is who the observer is, which is who we callobserveValueForKeyPathmethods
  • The second is what is the property of being observed, the property of being observed in general
  • The third is aoptionsThese are the parameters that we can set, and there are usually four of them
  1. NSKeyValueObservingOptionNewThe value before the change is supplied to the processing method
  2. NSKeyValueObservingOptionOldSupply the changed value to the processing method
  3. NSKeyValueObservingOptionInitialThe initialized value is provided to the handler, which is called once registered. Usually it takes the new value, not the old value.
  4. NSKeyValueObservingOptionPriorCall in 2 times. Before and after the value changes
  • The fourth is thecontextcontext

All the rest of it is easy to understand, but when we use this context, we usually just pass NULL, so what does it do? I’m sure Apple won’t write something useless here. Opening apple’s KVO documentation, we see the following

The idea here is that if we pass this parameter, it makes the performance safer and more straightforward. Why is that?

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [DMPerson new];
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.person.nick = [NSString stringWithFormat:@ % @ "+".self.person.nick];
    self.person.name = [NSString stringWithFormat:@ % @ "*".self.person.name];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context
{
    if (object == self.person) {
        if ([keyPath isEqualToString:@"nick"]) {
            NSLog(@" Nick changed \n%@",change);
        } else if ([keyPath isEqualToString:@"name"]) {
            NSLog(@"name changed \n%@",change); }}}Copy the code

To change the example above, I now need to look at the DMPerson object’s two properties, name and Nick, and perform different operations according to the different properties, which is more complicated to write in the observeValueForKeyPath. We need to first determine whether object is DMPerson, and then determine whether keyPath is Nick or name.

At this point, we can usecontextThis parameter

Change the code for registering observers

[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
Copy the code

Then change the code judgment in the callback

    if (context == PersonNickContext) {
        NSLog(@" Changed \n%@ by context Nick",change);
    } else if (context == PersonNameContext) {
        NSLog(@" changed \n%@ by context name",change);
    }
Copy the code

Look at the results

That’s right, the context is actually a marker that makes it easier and more straightforward to determine which attribute is changing the callback.

Remove observer

When using KVO, we usually need to remove observers when we don’t need them, and this is usually in the dealloc method. So is it okay if we don’t remove it? What could be the problem? Practice is the only truth, let’s try to find out

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [DMPerson new];
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.nick = @"mantou";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
    NSLog(@"DetailViewController :%@",change);
}
Copy the code

We write this code on a secondary page, and then every time we enter the screen, we click to exit the page, and repeat it several times. The results are as follows

That doesn’t seem to be a problem, so let’s change the code and seeDMWorkerObject properties change,DMWorkerIs a singleton

self.worker = [DMWorker shareInstance];
[self.worker addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
Copy the code

Do the same thing, and let’s see what happens

After the second entry and touch, the app crashed abnormally. Why is that? We went to apple’s official documentation and found an answer

An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.

When dealloced, the observer does not automatically delete itself. When the observer continues to send the notification, it may send a message to the freed observer, resulting in a memory access exception. Therefore, it is necessary to ensure that the observer is removed when it is destroyed.

  • When we first enteredDetailViewControllerWhen we registered an observer (withD1The touch triggers the callback, no problem, and we push out the page
  • When we enter the second timeDetailViewControllerWhen we first entered the registered observerD1It was destroyed, but becauseDMWorkerIt’s a singleton, so it’s still likeD1Send a callback message, resulting in abnormal memory access and application crash.

Conclusion: When our observer dealloc, always remove the observer

Automatic/manual listening for KVO

When we use the most basic way to use KVO, default is automatic monitoring mode, the manual monitoring and when we want to change into the model, we need to be monitored objects automaticallyNotifiesObserversForKey method

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    // You can distinguish between automatic and manual listening based on the key value
    if ([key isEqualToString:@"nick"]) {
        return YES;
    }
    return NO;
}
Copy the code

If we return NO directly, we will use all manual listening, which is why we touched the screen directly in the above case and there is NO response. If we want to respond, we need to implement the following method

[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
Copy the code

An assignment between willChangeValueForKey and didChangeValueForKey will be listened for manually.

Observe attributes that are influenced by multiple factors

We sometimes need to look at an attribute, but this attribute is influenced by multiple other factors. For example, for our file download process, download progress = downloads/total. If downloads and total numbers are constantly changing, how can we monitor the progress of downloads? Look at the following example

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [DMPerson new];
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.writtenData += 10;
    self.person.totalData += 30;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
    NSLog(@"DetailViewController :%@",change);
}
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"downloadProgress"];
}
Copy the code

We touched it three times, and let’s look at the print

We can see that each touch prints multiple pieces of data. Why? Let’s seeDMPersonThe implementation in

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData".@"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
- (NSString *)downloadProgress {
    if (self.writtenData == 0) {
        self.writtenData = 10;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f".1.0f*self.writtenData/self.totalData];
}
Copy the code

Actually very simple, in fact as long as in keyPathsForValuesAffectingValueForKey approach, Two factors will downloadProgress associated totalData and writtenData through setByAddingObjectsFromArray associated, so every time totalData or writtenData change, The system will automatically notify us that downloadProgress has changed. The reason we printed it three times the first time is because we set it to 100 when totalData is 0, which is one more call.

Observation of mutable arrays

What do we do if we need to look at mutable arrays? Go ahead and start testing

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [DMPerson new];
    self.person.dateArray = [NSMutableArray array];
    [self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
    [self.person.dateArray addObject:@"mantou"];
}
Copy the code

If you implement this code in viewDidLoad, you’ll theoretically see it and call back when you get to the page, but there’s actually no response. We found another line in apple’s documentation

In order to understand key-value observing, you must first understand key-value coding

KVO is built on KVC. KVO is built on KVC.

And in the official KVC document, there is such a paragraph

This means that if you need to use KVO to observe changes in the data type of the collection, you need to use the corresponding API to get the collection, so that the system can notify you when you set the value. So let’s change our code

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [DMPerson new];
    [self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
    self.person.dateArray = [NSMutableArray array];
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"mantou"];
}
Copy the code

The results are printed. The first time wasdateArrayThe initial print, the second isaddObjcetThe printAs you will have noticed, it was printed twicekindThe values are not the same, so what do they represent?

// *NSKeyValueChange*
typedef NS_ENUM(NSUInteger.NSKeyValueChange) {
    NSKeyValueChangeSetting = 1.NSKeyValueChangeInsertion = 2.NSKeyValueChangeRemoval = 3.NSKeyValueChangeReplacement = 4};Copy the code

This enumeration tells us the answer.

Exploration of underlying principles of KVO

We have illustrated many applications of KVO above, but what is its underlying principle? Let’s explore him now

Modify the ISA point

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[DMPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];
}
Copy the code

First we implement a simple KVO, and then we put a breakpoint on the addObserver method, and we want to explore what the system does after the addObserver method is called, so let’s look at its ISA point

We find that when calledaddObserverAfter that,self.persontheisaThe point has becomeNSKVONotifying_DMPerson. In previous explorations, we know,The relationship between the instance object and the class is actually that the isa of the instance object points to the class object. So here we can think roughly,self.personIn the calladdObserverMethod after has been fromDMPersonClass instance object, becomesNSKVONotifying_DMPersonInstance object of.

NSKVONotifying_DMPerson

So what is this NSKVONotifying_DMPerson thing? Did he exist directly from the beginning? What does he have to do with DMPerson? Let’s continue our exploration. Again, let’s see if NSKVONotifying_DMPerson existed in the first place

We can see clearly from this resultNSKVONotifying_DMPersonThis class is callingaddObserverMethod, the system dynamically adds a generated class. The next thing we know is that these two classes have very similar names, and the one that the system is dynamically generating, is it possible thatDMPersonWhat about subclasses of theta? Let’s print it outNSKVONotifying_DMPersonThe parent class

What a surpriseNSKVONotifying_DMPersonIs inherited fromDMPerson. So this middle class, is it possible to have its own subclass? Let’s look at this with the following code

#pragmaMark **- Traverses classes and subclasses **
- (void)printClasses:(Class)cls{
    // Total number of registered classes
    int count = objc_getClassList(NULL.0);
    // Create an array containing the given object
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // Get all registered classes
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}
Copy the code

The result is quite obvious, also confirms from another aspectNSKVONotifying_DMPersonisDMPersonThe subclass.

So, what’s in NSKVONotifying_DMPerson? We know that a class contains nothing more than member variables, methods, protocols, and so on. Let’s explore what methods are in it through the following code

#pragmaMark ** -traversal method -ivar-property**
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p".NSStringFromSelector(sel),imp);
    }
    free(methodList);
}
Copy the code

This code iterates over the methods in the class and prints the output. We know that if it’s an inherited method, and it’s not overridden, then it’s actually stored in the superclass, so with this method, we can’t print it out. So let’s take a look at the printout

So what we see here is that the system has been rewrittensetNickName,class,deallocThese methods, and added a name called_isKVOATo distinguish between classes that are not automatically generated by the system through KVO.

Remove observer change back to ISA point

We emphasized earlier that when observers are not used, they need to be removed, otherwise a memory access exception may occur. Since calling the addObserver method changes isa to point to a newly generated intermediate class, what does the system do when you remove the observer? We’re going to remove the observer from the dealloc method and put a breakpoint here, and then continue to observe the ISA pointing to self.person

We see that after removing the observer,self.persontheisaPointing back againDMPersonClass. So the intermediate class that was generated before, will it be released? Let’s go ahead and see

Through LLDB, we found that he still exists and has not been destroyed. The reason is that if you continue adding observers next time, the system will not regenerate a new intermediate class, but directly use this class, to prevent the waste of resources. This step is actually what we just sawNSKVONotifying_DMPersonIn thedeallocMethod overrides what’s done inside.

Overridden class method

We’ve looked at the overridden dealloc method, so what does the overridden class method mean?

Based on theaddObserverBefore and after the method callclassMethod of printing results we can see, thoughself.persontheisaHave been toNSKVONotifying_DMPersonBut, becauseNSKVONotifying_DMPersonRewrite theclassMethod, and the final printout isDMPersonThe goal is to hide the actions the system is doing behind the scenes, making developers less concerned about the bottom.

Override setter method

NSKVONotifying_DMPerson overrides several other methods, except for the last and most important override of the setter method. Before we do that, let’s consider a question. KVO is about observing the properties of an object. Can member variables be observed? We’ll find out by experimenting

@interface DMPerson : NSObject {
    @public
    NSString *name;
}
@property (nonatomic.copy) NSString *nickName;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [[DMPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@" Actual situation :%@ - %@".self.person.nickName, self.person->name);
    self.person.nickName = @"mantou";
    self.person->name = @"tong";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
    NSLog(@ "% @",change);
}
Copy the code

Here we declare a property nickName and a member variable name in DMPerson, and set KVO observation respectively. Let’s see the result

obviouslynickNameandnameAll of the assignments are in effect, but onlynickNameThe assignment to KVO triggers the KVO callback, so we can get a conclusion

KVO’s observations are for attributes only and have no effect on member variables

We know that self.person is actually an instance of NSKVONotifying_DMPerson, so the nickName property in the DMPerson class, Does that change when we call self.person.nickname = @”mantou”? We put a break point in dealloc after removing KVO

What we found was that after removing the observerisaPoints to theDMPerson, soDMPersonIn thenickNameThe property has actually changed, which is amazing, that we calledNSKVONotifying_DMPersonthesetNickNameMethod, but in the endDMPersonThe value of the property in is changed. Why? We’re callingsetNickNameAnd put a break point on

Then look at the stack information

You can see here, before we callNSKVONotifying_DMPersonthesetNickNameMethod after the system passesFoundationSome low-level processing in the framework eventually callsDMPersonthesetNickNameMethods. This is where the truth comes out.

KVO process summary

Let’s summarize the entire KVO process

  1. Just observation for properties, actually observationsettermethods
  2. After you set the observer, an intermediate class is automatically generated, usually namedNSKVONotifying_xxxxAnd points the instance object’s ISA to the intermediate class, which isObserved objectThe subclass.
  3. NSKVONotifying_xxxxThe rewrite theclassMethods,deallocMethod and the properties being observedsetterMethod, and generates a_isKOVAIdentification method of
  4. Calling intermediate classesNSKVONotifying_xxxxthesetterMethod is actually called from thereThe observedThe properties of thesettermethods
  5. And finally when the observer is released, you need to remove the observer, and thenisaIt’s going to go backThe observedThe intermediate class will not be removed. If you continue to add observations next time, you can use the intermediate class directly without having to regenerate it