1. Introduction of KVO

KVO: key-value-observing is a mechanism that allows you to notify other objects of specified property changes. Key-value observations provide a mechanism for notifying objects of specific property changes of other objects. It is particularly useful for communication between the model layer and the controller layer in an application. Controller objects typically observe the properties of model objects, and view objects observe the properties of model objects through controllers. In addition, however, model objects may observe other model objects (often used to determine when a dependency value changes) or even themselves (again used to determine when a dependency value changes). You can look at properties, including simple properties, one-to-one relationships, and multi-type relationships. Observers exposed to many relationships are informed of the types of changes made and which objects are involved in the changes. Because KVO is not open source, please refer to the official documentation

2. Common API usage

To take a simple example, we have a Person class that has an Account class with attributes like balance in it. We ask the account to add an observer, Person, to tell the Person when the money in the account changes, and remove the observer when it is no longer needed.

2.1 registered

[self.account addObserver:self forKeyPath:@"balance" options:NSKeyValueObservingOptionNew context:NULL];
Copy the code

Let person look at the balance of the account, where options

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01.// When the observed property is sent to change, we return its latest change
NSKeyValueObservingOptionOld = 0x02.// When the observed property changes, we return the value before it changed

NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0=))0x04.// Using it in one-to-one situations immediately sends a notification to the observer; When this option is with - addObserver: toObjectsAtIndexes: forKeyPath: options: context when used together, add to the observer each index send notification object.
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0=))0x08 // Send separate notifications to observers before and after each change

}
Copy the code

The context pointer in the message contains arbitrary data that will be returned to the observer in the corresponding change notification. You might specify NULL and rely entirely on the key path string to determine the source of the change notification, but this approach can cause problems for objects whose superclass is also observing the same key path for different reasons. Context is a nullable void * type. We can pass NULL, which represents a unique identifier, so we can find it directly. For example, an observer is observing multiple objects but they all call the same method.

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;

static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
Copy the code

2.2 notice

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

    NSLog(@"% @",change);

}

Copy the code

In order to receive from the Account change notice, the Person realizes the observeValueForKeyPath: ofObject: change: context: method, this is all observers need to be. The Account sends this message to Person when one of the registered key paths changes. The Person can then take appropriate action based on the change notification.

2.3 remove

[self.account removeObserver:self forKeyPath:@"balance"];
Copy the code

In the end, when it no longer needs to notice, at least before it was released from distribution, the Person instance must by sending messages to the Account removeObserver: forKeyPath to cancel the registration.

2.4 Manual change notification

NSObject provides a basic implementation for automatic notification of key value changes. Automatic key-value change notification notifies the observer of changes made with key-value matching accessors and key-value encoding methods. For example: setter methods, KVC. In some cases, you may want to control the notification process, for example, by minimizing the triggering of notifications that are not necessary for application-specific reasons, or by grouping some changes into a single notification. Manual change notifications provide the means to do this

// Automatic switch

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {

    BOOL automatic = NO;

    if ([theKey isEqualToString:@"balance"]) {

        automatic = NO;// Specify keypath

    }

    else {

        automatic = [super automaticallyNotifiesObserversForKey:theKey];

    }

    return automatic;

}
// Implements the instance accessor method for manual notification
- (void)setBalance:(double)theBalance {

    [self willChangeValueForKey:@"balance"];// Will change

    _balance = theBalance;

    [self didChangeValueForKey:@"balance"];

}
Copy the code

2.5 One-to-one Relationship

In many cases, the value of an attribute depends on the value of one or more other attributes in another object. If the value of an attribute changes, the value of the derived attribute should also be marked as changed. How you ensure that key-value observation notifications are published for these dependent properties depends on the cardinality of the relationship. For example, a person’s full name depends on first and last names. The method for returning the full name can be written as follows:

- (NSString *)fullName {

    return [NSString stringWithFormat:@"% @ % @",firstName, lastName];

}
Copy the code

The application observing the fullName attribute must be notified when the firstName or lastName attributes change, because they affect the value of the attribute. One solution is to cover keyPathsForValuesAffectingValueForKey: specify a person’s fullName attribute depends on the lastName and firstName attribute

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 

    if ([key isEqualToString:@"fullName"]) {

        NSArray *affectingKeys = @[@"lastName"The @"firstName"];

        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];

    }

    return keyPaths;

}
Copy the code

Normally you should call super and return a collection containing any members that are created as a result of doing so (so as not to interfere with the overwriting of this method in the superclass). You can also by implementing the following naming convention keyPathsForValuesAffecting < Key > class methods to achieve the same results, including the < Key > is dependent on the value of the attribute (capitalize the first letter) name, for example fullName:

+ (NSSet *)keyPathsForValuesAffectingFullName {

    return [NSSet setWithObjects:@"lastName"The @"firstName", nil];

}
Copy the code

2.6 One-to-many Relationship

The above method does not support multiple relationships. For example, Person has multiple accounts, and we hope to immediately tell Person how much my totalSalary is when salary is paid to each account.

@interface KBPerson : NSObject
@property (nonatomic, assign) NSNumber *totalSalary;

@property (nonatomic, strong) NSArray *accounts;
@end
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

 

    if ([keyPath isEqualToString:@"salary"]) {

        [self updateTotalSalary];

    }

    else{}} - (void)updateTotalSalary {

    [self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];

}
// Implement it
    KBAccount *account1 = [[KBAccount alloc]init];

    account1.salary = 12;1 / / account

    KBAccount *account2 = [[KBAccount alloc]init];

    account2.salary = 1;2 / / account

    self.person.accounts = @[account1,account2];

    [account1 addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:fullNameContext];

    [account1 addObserver:self.person forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:NULL];// Tell person the payday has been paid

    [account2 addObserver:self.person forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:NULL];

    [self.person addObserver:self forKeyPath:@"totalSalary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];// Tell the current self, total Salary to change
// Simulate changes- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

{

    KBAccount *account1 = self.person.accounts[0];

     NSInteger salary = arc4random()%100;
     
    KBAccount *account2 = self.person.accounts[1];

     NSInteger salary2 = arc4random()%100;

    NSLog(@"Account 1 is paid: %ld",salary);

    account1.salary = salary;

    NSLog(@"Account 2 is paid :%ld",salary2 );

    account2.salary = salary2;

}
// Listen for changes
/ / change

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{

    

    if (context == totalSalaryContext) {

        NSLog(@"Total assets in personal account: %@",[change objectForKey:NSKeyValueChangeNewKey]);

    }else if(context == fullNameContext){

        NSLog(@"Changed: %@",[change objectForKey:NSKeyValueChangeNewKey]); }}Copy the code

2.6 Mutable array observation

When our observer’s properties are mutable arrays, adding them directly does not call setter methods and does not trigger kVO notification callbacks

self.person.dateArray = [NSMutableArray arrayWithCapacity:1];

[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];

[self.person.dateArray addObject:@"1"];/ / not to take effect
Copy the code

The official recommendation is that we use mutableArrayValueForKey, which triggers the kvo callback via KVC.

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

    self.person.nick = [NSString stringWithFormat:@"% @ +",self.person.nick];

// self.person.writtenData += 10;

// self.person.totalData += 1;

    // KVC collection array


    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];

}
Copy the code

One of thekindsaidThe type of key value change, is an enumeration, there are four main types

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1./ / set value
    NSKeyValueChangeInsertion = 2./ / insert
    NSKeyValueChangeRemoval = 3./ / remove
    NSKeyValueChangeReplacement = 4./ / replace
};

Copy the code

3. Underlying principles

View official Documents

Kvo uses a technique calledisa-swizzing“Technology to achieve.

  • isaThe pointer, as the name implies, points to the one that maintains the dispatch tableThe class of the object. This dispatch table essentially containsA method that points to a class implementationAnd other data.
  • Property of the observed object when the observer registers its propertiesisaPointer to theHas been modifiedTo point toThe middle class, instead of the real class. As a result,isaThe value of a pointer does not necessarily reflect the actual class of the instance.
  • You shouldn’tisaPointer to determineThe members of the class. Instead, you should useClass method to determine the class of an object instance.

3.1 KVO triggering mechanism

We looked at the documentation earlier to say that KVO is triggered by setter methods. Let’s verify that

self.student = [[KBStudent alloc]init];

    [self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];

    [self.student addObserver:self forKeyPath:@"nickName"options:NSKeyValueObservingOptionNew context:NULL]; - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

{

    self.student.name = @"jack";

    self.student->nickName = @"Smith";
    
 }
Copy the code

Member variables passTranslation memoryUnable to triggerkvoThe callback,setterThere is a callback. We use thekvoAnd implementsettermethods

- (void)setNickName:(NSString*)nickName
{
    self->nickName = nickName; 

}
Copy the code

Conclusion:KVOThe trigger is monitoredsetterMethod, only implementedsetterMethod to trigger.

3.2 the middle class

The official documentation states that KVO is implemented by swapping isa for instance objects, which generates an intermediate class. Let’s print out the before and after changes

#pragma mark - 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

printKBStudentAnd subclasses of it, added one moreNSKVONotifying_KBStudentAnd this is the middle class, which is the middle classKBStudentThe subclass. Let’s check LLDB

Add our instance object beforeisaPoint to the0x000000010e1551e8 KBStudentClass, added to point toNSKVONotifying_KBStudentIn the class.

3.3 What’s in the middle class?

Does it continue the methods, attributes, and member variables of the parent class?

#pragma mark -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);

}
// Prints member variables- (void)printClassAllIvars:(Class)cls{

    

    unsigned int count = 0;

    Ivar *ivars = class_copyIvarList(cls, &count);

    

    for (int i = 0; i<count; i++) {

        Ivar ivar = ivars[i];

        NSString *ivarName = [NSString stringWithFormat:@"%s", ivar_getName(ivar)];

        NSLog(@"% @",ivarName);

    }  

    free(ivars);

}
// Print attributes- (void)printClassAllPropertys:(Class)cls{

    unsigned int count = 0;

    objc_property_t *propertys = class_copyPropertyList(cls, &count);

    

    for (int i = 0; i<count; i++) {

        objc_property_t property = propertys[i];

        NSString *propertyName = [NSString stringWithFormat:@"%s", property_getName(property)];

        NSLog(@"% @",propertyName);

    }

    free(propertys);
    

}
Copy the code

Prints the result, not inheriting the parent class exactly. It’s understandable, actually, that we areKBStudentAdd observation main observationkeypathisnameThat’s all we need to worry about in the middle classsetNameThe implementation of.Class (get the current class)Dealloc (Destroy)._isKVOA (check whether kVO)Is to rewriteNSObject.

Why rewrite and not inherit?

1. We can see that the addresses of the printing methods are not the same.

2. The subclass inherits the parent class method itself does not rewrite, the subclass method list is not, call the time to look up the parent class chain.

3.4 What does the intermediate class setter method do?

We’re changing the name externally, which is calling the setter for name, because the isa swap is actually calling the setter for the middle class, but we’re also changing the name property for KBStudent, otherwise we’re going to have a problem, the middle class is pretty housekeeping, Take care of hind still need to tell host.

self.student.name = @"jack";
/ / to enter- (void)setName:(NSString *)name
{

    _name = name;

    NSLog(@"%s",__func__);

}
Copy the code

It can be concluded that:

  1. When the outside callssetterMethod, the actual entrySetter methods for intermediate classesTo implement.
  2. The middle classsetterThe method will go firstnoticeThe outsideThe observerThe value of your observation is going to change.
  3. Sends a message to the parent classCall the setter of the parent classMethods, practical change.

3.5 Is the middle class destroyed when the observer dealloc?

We remove it in our dealloc controller, and hit a breakpoint.

The isa of the removeObserver instance object still points to the intermediate class NSKVONotifying_KBStudent. Removed:

We’re going back to what we had beforeisaPoint, pointThis class.

  • Why do we need instance objects to be destroyedremovereductionisaTo prevent wild Pointers. For example, our student isa simple interest. If we do not restore isa, the next access will crash.

So let’s remove and go back to the previous page and print out the middle class and see if it exists

I didn’t destroy it, which is understandable, but it’s kind of complicated to create this class, so destroy it. Users constantly coming in and out of a page, constantly adding and removing, can consume a lot of memory,Waste of memory.

4. To summarize

  • kvoIt’s like we’re monitoring a value of interest in real time, based on that valueSetter method listeningWhen called to tell the observer that the value is about to change, remove the observation when not in use.
  • Kvo internal implementation is through aThe middle classThe setter for the middle class is basicallyNotifies the observer of a value change.Call the setter of the parent classMethod, actually change. This design is equivalent to an intermediate class acting as a butler, as long as the external instructions, the specific implementation of the butler,The coupling is reduced.
  • kvoremovemainlyReduction of the isa, restore the isa of the instance object from the intermediate class toThis classThe middle class,Don't destroyTo facilitate the next association.