This is the ninth day of my participation in the August More text Challenge. For details, see: August More Text Challenge

KVO profile

KVO means key-value Observing. KVO is a mechanism that allows objects to be notified when specified properties of other objects are allowed to change; In order to understand key-value observation, you have to understand key-value encoding which is KVC;

KVC is the key-value coding, which can dynamically assign values to the attributes of the object after the creation of the object, while KVO is the key-value observation, which provides a set of listening mechanism. When the specified attributes of the object are modified, the object will receive notification. Therefore, it can be seen that KVO listens for the dynamic changes of the object attributes based on KVC.

That sounds a little bit like NSNotificationCenter, so what’s the difference?

KVO is different from NotificationCenter

  • The same
    • Both of themObserver mode, are used toListening to the;
    • Can achieveMore than a pair ofThe operation;
  • The difference between
    • KVOUsed to listen for changes in an object’s property, and the property name is a stringNSStringTo do a search;
    • NSNotificationCentertheListening to theThat isPOSTWe can control the operation, andKVOIs made up ofsystemControl;
    • KVOCan recordThe old and new valuesThe change of the;

The use of the KVO

KVO basic use

  • Registered observer
[self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];
Copy the code
  • Listen for KVO callbacks
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"nickName"]) {
        NSLog(@"%@ changed :%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else{[superobserveValueForKeyPath:keyPath ofObject:object change:change context:context]; }}Copy the code
  • Remove observer
- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"nickName" context:NULL];
}
Copy the code

The context of use

Context is described in the KVO official documentation as follows

In short, a context is a pointer that contains any data that will be passed back to the observer in the corresponding change notification. We can set the context to NULL and use the keyPath (keyPath) to determine the source of the change notification, but this may cause problems for objects whose parent class listens to the same keyPath. Therefore, we can set up a different context for each of the observed keyPath, thus completely skipping the comparison of the keyPath and directly using the context for more efficient notification parsing. Context is more secure and extensible, which greatly improves performance and code readability.

Let’s get an intuition from the code:

  • Don’t usecontextWhen code logic:
- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[Person alloc] init];
    self.student = [[Student alloc] init];

    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];
    [self.student addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"nickName"] && object == self.person) {
        NSLog(@"Person:%@ changed :%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else if ([keyPath isEqualToString:@"nickName"] && object == self.student) {
        NSLog(@"Student:%@ change :%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else{[superobserveValueForKeyPath:keyPath ofObject:object change:change context:context]; }} - (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"nickName" context:NULL];
    [self.student removeObserver:self forKeyPath:@"nickName" context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.nickName = @ "nickname";
    self.student.nickName = @" Classmate Wang";
}
Copy the code

In order to listen for the same nickName property for both the Person and Student classes, we need to use object to identify the source object in addition to using keyPath when we receive the listening callback. To accurately distinguish whose nickName changed;

  • usecontextWhen code logic:
/ / defines the context
static void *PersonNameNickContext = &PersonNameNickContext;
static void *StudentNameNickContext = &StudentNameNickContext;

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[Person alloc] init];
    self.student = [[Student alloc] init];

    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNameNickContext];
    [self.student addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:StudentNameNickContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == PersonNameNickContext) {
        NSLog(@"Person:%@ changed :%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else if (context == StudentNameNickContext) {
        NSLog(@"Student:%@ change :%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else{[superobserveValueForKeyPath:keyPath ofObject:object change:change context:context]; }} - (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"nickName" context:PersonNameNickContext];
    [self.student removeObserver:self forKeyPath:@"nickName" context:StudentNameNickContext];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.nickName = @ "nickname";
    self.student.nickName = @" Classmate Wang";
}
Copy the code

The above method does not trigger KVO;

The nickName of the object can be determined by the context alone.

KVO usage details

Remove the need for observers

The KVO documentation describes removeObserver as follows:

Explanation:

Sent through to the observed removeObserver: forKeyPath: context: news, specify the observation object, the key path and the context, you can delete a key value observer;

Upon receiving the removeObserver: forKeyPath: context: news, people will no longer receive any observeValueForKeyPath: the specified key path and object ofObject: change: the context information;

When removing an observer, note the following:

  • If there is no registered observer, being removed causesNSRangeException, which can be invoked onceremoveObserver:forKeyPath:context:To deal withaddObserver:forKeyPath:options:context:; If this is not feasible in the project, you can useremoveObserver:forKeyPath:context: callOn thetry/catchBlock to handle potential exceptions;
  • When released, the observer does not automatically release itself. The observed object continues to send notifications regardless of its state. However, like any other message sent to a released object, change notifications can trigger a memory access exception. Therefore, you should make sure that the observer deletes itself before it disappears from memory.
  • The protocol cannot ask whether an object is an observer or an observed. Code is constructed to avoid release-related errors, a typical pattern being that during observer initialization (initorviewDidLoadIn) to register as an observer and during the release process (usually indeallocIn) logout, to ensure that the add and delete messages are paired, and to ensure that the observer is unregistered and freed from memory before being registered.

In general, KVO registered observers and removed observers need to be in pairs, if only registered but not removed, there will be a crash

In the figurePersonSingletons are used to prevent release and demonstrate the crash phenomenon

The reason for the crash is that the KVO observer was not removed after the first registration. When entering the interface again, the KVO observer was registered a second time. The previously registered object was not released, resulting in duplicate registered observers.

So, to prevent this, it is recommended to remove observers from dealloc. Note that if a context is used, then remove it using the same context, otherwise it will crash and raise an exception named NSRangeException:

KVO automatic trigger and manual trigger

  • Automatic trigger
// Automatic switch
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}
Copy the code

If the method returns YES, it can be listened on, and NO, it cannot be listened on.

  • Manual trigger
- (void)setNickName:(NSString *)nickName {
    [self willChangeValueForKey:@"nickName"];
    _nickName = nickName;
    [self didChangeValueForKey:@"nickName"];
}
Copy the code

When the automatic trigger method returns NO, we can do this manually;

KVO observation: one to many

One to many in KVO observation means that by registering one observer, you can listen to multiple attribute changes;

For example, when downloading a file, we often need to calculate the downloadProgress based on the total number of downloads and the current number of downloads, writtenData. There are two ways to do this:

  • The first is to add an observer to each of the two properties and calculate the current download progress when either of them changesdownloadProgress;
  • Second, implementationkeyPathsForValuesAffectingValueForKeyMethod, combining two observers into one, that is, observing the currentdownloadProgresswhentotalDataandwrittenDataA notification is sent when any of the values change:
@implementation Person

+ (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];
}
@end


@implementation SecondViewController

static void *PersondownloadProgressContext = &PersondownloadProgressContext;

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = NSStringFromClass(self.class);
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.person = [[Person alloc] init];
    
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:PersondownloadProgressContext];

}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context == PersondownloadProgressContext) {
        NSLog(@"Person:%@ changed :%@", keyPath, [change objectForKey:NSKeyValueChangeNewKey]);
    } else{[superobserveValueForKeyPath:keyPath ofObject:object change:change context:context]; }} - (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"downloadProgress" context:PersondownloadProgressContext];
}

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

@end
Copy the code

KVO looks at mutable arrays

KVO is based on KVC, so adding an element to a mutable array, calling the addObject method directly doesn’t trigger the setter method, so you can’t listen to what’s happening to the array when you’re adding an element through addObject;

@implementation SecondViewController

static void *PersondateArrayContext = &PersondateArrayContext;

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = NSStringFromClass(self.class);
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.person = [[Person alloc] init];
    self.person.dataArray = [[NSMutableArray alloc] initWithCapacity:0];
    
    [self.person addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew context:PersondateArrayContext];

}

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

- (void)dealloc {
    [self.person removeObserver:self forKeyPath:@"dataArray" context:PersondateArrayContext];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.person.dataArray addObject:@ "1"];
}
Copy the code

The mutableArrayValueForKey method is described in the KVC official documentation.

Modify the code as follows:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [[self.person mutableArrayValueForKey:@"dataArray"] addObject:@ "1"];
}
Copy the code

Running result:

Array changes can be heard;

Where kind represents the type of key change, which is an enumeration type:

Typedef NS_ENUM (NSUInteger NSKeyValueChange) {NSKeyValueChangeSetting = 1, / / setting NSKeyValueChangeInsertion = 2, / / insert NSKeyValueChangeRemoval = 3, / / remove NSKeyValueChangeReplacement = 4, / / replace};Copy the code

So, the kind of general property is 1;

KVO principle exploration

The KVO principle is described in the KVO official document as follows:

Resolution:

  • KVOtheAutomatic key value observationIs the use ofisa-swizzlingTechnical realization;
  • isaThe pointer, as the name suggests, points to maintenanceScheduling tableThe object ofclass. thisScheduling tableIt involves pointing, essentiallyA pointer to a method implemented by the, as well asOther data;
  • whenobjecttheattributeRegistered as aThe observer, will be modifiedObserved objecttheisaPointer to oneThe middle classNot a real class. As a result,isaThe value of the pointer does not necessarily reflect the actual class of the instance;
  • Should not rely onisaPointer to determineThe members of the class. Instead, useClass methodTo determine theClass of object instance;

KVO code debugging

Attribute to observe

In the previous article, we tested that the change to nickName can be heard by KVO, so can the member variable also be heard?

Add a member variable named name to the Person class:

@interface Person : NSObject {
    @public
    NSString *name;
}
@property (nonatomic.copy) NSString *nickName;
@end
Copy the code

Call (name, nickName, KVO, nickName)

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

Running result:

KVO can only listen on attributes, not member variables; The difference between a property and a member variable is that a property has one more setter than a member variable, and KVO listens to that setter.

The middle class NSKVONotifying_Xxxx

According to the KVO official documentation, after the observer is registered, the isa pointer of the observed object changes.

Before registering as an observer, the isa of the instance object Person points to Person;

After registering as an observer, the isa of the instance object Person points to NSKVONotifying_Person;

After registering the observer successfully, the isa of the instance object points to an intermediate class, NSKVONotifying_Xxxx, and the isa pointer does change

NSKVONotifying_Xxxx research

The relationship between NSKVONotifying_Xxxx and the observer

So what exactly is NSKVONotifying_Xxxx?

From the above two diagrams, we can determine that NSKVONotifying_Person is indeed generated automatically by the system at the bottom after the Person object is registered as an observer. So what does this class have to do with Person?

Let’s first iterate to see if the subclasses of Person change before and after registering an observer:

Person has a subclass named NSKVONotifying_Person. Person has no subclass named NSKVONotifying_Person.

Method list for NSKVONotifying_Xxxx

So, what are the methods in NSKVONotifying_Person:

{setNickName, class, dealloc, _isKVOA}}

  • setNickNameObserve the setter methods of an object
  • classtype
  • deallocWhether to release (when this dealloc executes, isa is redirected to Person)
  • _isKVOADetermine whetherKVOAn identification code generated

So are these methods inherited or do they override methods of the parent class?

Create class Student that inherits from class Person:

@interface Student : Person

@end

@implementation Student

@end
Copy the code

Then print a list of methods for Student and NSKVONotifying_Person, respectively:

Student’s method list is not printed (the integrated method cannot be traversed through the subclass’s method list), indicating that the methods in NSKVONotifying_Person override the methods of the superclass

Release problem of NSKVONotifying_Xxxx

Now, the system automatically creates NSKVONotifying_Person

After removing the observer from dealloc, the ISA pointer is redirected to the Person class, so is NSKVONotifying_Person destroyed?

Let’s print a subclass of Person one level above the current interface:

As you can see, even though the dealloc method executes and the observer has been removed, NSKVONotifying_Person still exists after returning to the parent interface;

Intermediate classes, once generated, are always there for reuse purposes and are not destroyed;

Setter method ownership problem

We have already verified that KVO listens to the setter, and that the intermediate class NSKVONotifying_Person overwrites the setter, so is the final setter of NSKVONotifying_Person or Person?

As you can see, when you remove the observer, isa is already pointing to Person, and the nickName value has changed, so the setter method is Person;

Next, let’s verify by observing the change of variable values:

Run the project, breakpoint, and watch the _nickName value change:

Continue running the project, triggering the listen:

Bt print stack information:

So the final setter that’s called is Person’s setNickName method;