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

The definition of KVO

In iOS development, KVO mode is often used to process some business logic by listening for the change of the property value of the object. How does KVO listen? How are listeners notified of changes in property values? Firstly, the definition of KVO is based on the official apple key-value Observing Programming Guide.

Key-value observing provides a mechanism for notifying objects when specific properties of other objects change.

The flow chart of KVO

According to the above definition, it is easy to draw a flowchart of KVO

The use of the KVO

First let’s take a look at the code use of KVO, directly call the relevant API, the code is as follows:

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

So it’s very simple, just add the addObserver method to the viewDidLoad method, listen for the Name property of the Person object, and we’re always passing the Context NULL, so what does the Context do?

The Context of KVO

Let’s first look at the official documentation for the definition of Context.

In the addObserver: forKeyPath: options: context: the callback function returns data, observers will receive to contain any data. You can specify NULL and rely entirely on keyPath to determine the source of the change notification, but this approach can cause problems for the object because the object’s superclass also observes the same keyPath for different reasons. A safer and more extensible approach is to use context to ensure that received notifications are addressed to the observer, not the superclass.

When adding multiple KVO listeners to a class, we can therefore specify the Context to determine the source of the listener properties, avoiding too much judgment logic in the same callback method.

The sample code
static void *PersonHobbyContext = &PersonHobbyContext;
static void *PersonNameContext = &PersonNameContext;
Copy the code
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[ATPerson alloc] init];
    [self.person addObserver:self
                  forKeyPath:@"hobby"
                     options:NSKeyValueObservingOptionNew
                     context:PersonHobbyContext];
    [self.person addObserver:self
                  forKeyPath:@"name"
                     options:NSKeyValueObservingOptionNew
                     context:PersonNameContext];
}
Copy the code
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if (context == PersonHobbyContext) { // hobby } else if (context == PersonNameContext) { // name } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; }}Copy the code

By specifying the Context above, we can listen for changes of different attribute values in the same object. When the attribute values change, we do not need to determine through multiple layers of judgment, which can improve the readability of the code and reduce redundant code.

Remove observer

Whether you need to remove the observer during the destruction of the class or see the official documentation for the removal of the observer.

  • Removing an observer causes an NSRangeException if no observer is registered. When you areaddObserver:forKeyPath:options:context:Will callremoveObserver:forKeyPath:context:Once, or if this is not available in your application, putremoveObserver:forKeyPath:context:Call ontry/catchBlock to handle potential exceptions.
  • The observer does not automatically remove itself when released. The observed object continues to send notifications, regardless of the observer’s state. However, as with any other message, a change notification sent to a freed object triggers a memory access exception. Therefore, you can ensure that the observer removes itself before it is freed from memory

Here’s an example to verify what happens if you don’t remove the listener.

Remove the listening case analysis

Code section

The homepage HomeVC

- (void)viewDidLoad { [super viewDidLoad]; Self. title = @" "; The self. The navigationItem. RightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle: @ "next page" style: UIBarButtonItemStylePlain target:self action:@selector(nextClick:)]; } - (void)nextClick:(UIBarButtonItem *)item { [self.navigationController pushViewController:[[SecondVC alloc] init] animated:YES]; }Copy the code

SecondVC

- (void)viewDidLoad { [super viewDidLoad]; self.title = @"KVO"; Self. Person = [ATPerson sharedInstane]; [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"SecondVC -- %@", change); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { self.person.name = @"hello world"; }Copy the code
steps
  1. Click on the home pageThe next pageA push toSecondVC
  2. Tapping the screen triggers a listening callback
  3. Return to the home page and enter again
  4. Tapping the screen triggers the listening callback again

Run, first entrySecondVCPage click screen normal when back toHomeVCClick in againSecondVCClick screen programCrash.

Cause Analysis of crash

The reason for the second Crash was that we set a singleton for Person. The first time, when the pop came out, the original SecondVC memory was released. The second time, we re-entered and opened a new memory space for SecondVC, but we did not remove the listening when dealloc. Person will still send a listening message to the original SecondVC, accessing the original released address and causing a Crash. Although this Crash will certainly occur, for the sake of code robustness, note the removal of listening operations.

Note: In any case, if you add a KVO listener to an object, remember to remove the listener when the object is destroyed.

Manual observation

KVO can also manually control the corresponding properties of listening. The automatic listening function needs to be turned off, which is enabled by default.

Manual control

Automatic listening function, need to set to return NO

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}
Copy the code

When you need to listen on a specified property, you need to re-setter the method on the object to listen on, as follows:

Code sample

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}
Copy the code

When automaticallyNotifiesObserversForKey returns NO KVO will only listen to a custom implementation of setter method, other attribute values will be ignored.

KVO’s one to many observations

When the observation object attribute values between associated, in this case you need to implement the KVO keyPathsForValuesAffectingValueForKey function, rewrite KeyPaths associated attribute values.

You can override this method when the getter method of a property calculates the value to be returned using the values of other properties, including properties located by the key path. Your override should normally call super and return a collection containing any members of the result of doing so (so as not to interfere with the override of this method in the superclass).

Here is a simple download example to demonstrate.

The sample code

Add 3 properties to the ATPerson class, current download progress = downloaded/total data, change the corresponding property value when clicking on the screen, and print the relevant information in the KVO listening callback.

ATPerson.h

@interface ATPerson : NSObject @property (nonatomic, copy) NSString *downloadProgress; @property (nonatomic, assign) double writtenData; @property (nonatomic, assign) double totalData; // Total data @endCopy the code

ATPerson.m

/ / rewrite KVO surveillance attribute value + (NSSet < > nsstrings * *) keyPathsForValuesAffectingValueForKey: (nsstrings *) 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 [[nsstrings alloc] initWithFormat: @ "% f", 1.0 f * self writtenData/self totalData]; }Copy the code

Change the change in the attribute value in the touchBegin event of HomeVC.

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    self.person = [[ATPerson alloc] init];
    [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  += 1;
}

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

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

Run the code inHomeVCClick the screen to view the log, you can see that the download progress can be monitored without clicking once.

Observations of mutable arrays

Next we add a variable array property, dataArray, to ATPerson and observe this property in HomeVC.

The sample code

HomeVC.m

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[ATPerson alloc] init];
    [self.person addObserver:self
                  forKeyPath:@"dateArray"
                     options:NSKeyValueObservingOptionNew
                     context:NULL];
    self.person.dateArray = [[NSMutableArray alloc] init];
    [self.person.dateArray addObject:@"hello"];
}

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

Run, see print information mutable arraydataArrayNo information is added.

Analysis of the

According to the previous KVO attribute monitoring methods, they all adopted the same way, but when actually listening to the mutable array, we found that the result did not change the new value. With doubt, check apple’s official documentation about KVO, and there is the following prompt first in the documentation introduction.

Important: To understand KVO, you must first understand KVC.

So go back to the KVC documentation and provide the following methods for viewing key values for collection types.

1. MutableArrayValueForKey: and mutableArrayValueForKeyPath: 2. MutableSetValueForKey: and mutableSetValueForKeyPath: 3. MutableOrderedSetValueForKey: and mutableOrderedSetValueForKeyPath:Copy the code

Next, use this method to listen on the mutable array, the code is as follows, run again:You can see that you are listening for a change in the property value of the mutable array.

conclusion

According to the definition of KVO in official documents, the actual case code is used to verify the use of KVO. The observation of KVO key value is combined with the use of KVC, and the observation of KVO attributes shall refer to the implementation principle of KVC. Through the above analysis, the following conclusions are made:

  • We can listen for properties of an object, regular property calls directlyaddObserverSpecial attributes need to be defined in KVC, such as collection classes.
  • Be careful to remove the listener during object destruction
  • You can set the listening mode manually
  • If an attribute depends on multiple attribute values, it needs to be overriddenkeyPathsForValuesAffectingValueForKeyMethods.

The above is the basic introduction of the use of KVO in this paper, and the analysis of the principle of KVO will be explored in the next chapter.