KVO profile

Apple official documentation KVO introduction

Key-value observing (KVO) is a mechanism that allows an object to be notified when the specified properties of other objects change.

KVO details

Options

Options affects the contents of the change dictionary provided in the notification and how the notification is generated.

  • NSKeyValueObservingOptionNew: gets the changed value
  • NSKeyValueObservingOptionOld: gets the value before the change
  • NSKeyValueObservingOptionInitial: Observe the initial value (register the method to handle notifications immediately called)
  • NSKeyValueObservingOptionPrior: called once before and after a value change

Context

When the addObserver: forKeyPath: options: context: method are introduced to NULL context, in observeValueForKeyPath: ofObject: The change:context: method can handle callbacks using object and KeyPath. If you pass in a unique identifier context, you can use the context directly to distinguish between callbacks.

Remove observer

When the current page release need to remove the observer, or when the observed object is a singleton, page (observers) is released, the singleton exists, the registration is still effective, last time the page was again registered as observers, would be considered to have two observers, the attributes change, trigger notifications, but the first observer has been released, This causes wild pointer access.

Manual/automatic

It is enabled automatically by defaultKVOWhen you want to manually controlKVOWhen, except above to returnNOAnd also do some processing on the observed properties, like in my case, the observed propertiesageFor example:

KVO App (Download progress)

  1. Define an observed propertydownloadProgress, and two other attributes (totalData,writtenData) is used to calculate progress
@interface DownLoader : NSObject
@property (nonatomic.copy) NSString *downloadProgress;
@property (nonatomic.assign) double writtenData;
@property (nonatomic.assign) double totalData;
@end
Copy the code

2. Implement the getter method of downloadProgress

- (NSString *)downloadProgress{
    return [[NSString alloc] initWithFormat:@"%f".1.0f*self.writtenData/self.totalData];
}
Copy the code

3. Configure monitoring totalData and writtenData, and obtain downloadProgress by calculation. That is, downloadProgress changes depend on totalData, writtenData.

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData".@"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
Copy the code

Use 4.

Look at mutable arrays

usemutableArrayValueForKeyGets an array and performs array manipulation.

KVO underlying principles

KVO registered

That is, instance objectspregisteredKVO, a subclass is generatedNSKVONotifying_Person, named asNSKVONotifying_<className>.

KVO subclass

NSKVONotifying_PersonWhat is being implemented?Override the following methods:

  • SetAge::
  • classWill:KVOGenerated subclassesisaA pointer to the current class is verified as follows:

  • dealloc: called when destroying.
  • _isKVOA: An identifier indicating that the current class isKVOGenerated subclasses

Explore a class method hereSetAge:The call.Pictured above,[Person setAge:]These methods are also called before the call:

[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] 
[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
_NSSetLongLongValueAndNotify
Copy the code

In other words, the setAge method of the dynamically generated subclass of KVO will call the above three methods internally, and then call the setAge method of the parent class again.

Call stack (); call ();

NSKeyValueDidChange
NSKeyValueNotifyObserver
[ViewController observeValueForKeyPath:ofObject:change:context:]
Copy the code

Finally, the sequence of setAge listener calls is as follows:

1.[NSKVONotifying_Person setAge:]
2._NSSetLongLongValueAndNotify
3.[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
4.[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] 
5.[Person setAge:]
6.NSKeyValueDidChange
7.NSKeyValueNotifyObserver
8.[ViewController observeValueForKeyPath:ofObject:change:context:]
Copy the code

NSKeyValueDidChange calls after the call, send a notification call _observeValueForKeyPath: ofObject: changeKind: oldValue: newValue: indexes: context: complete notification processing.

KVO removed

When removing theKVOWhen listening, the class becomes original, that is, when registered, the instance object’sisaWill point to a subclass of the current classNSKVONotifying_<className>When you remove the wiretap,isaLet’s go back to the previous class.

Custom KVO

#import <Foundation/Foundation.h>
#import "MWKVOInfo.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (KVO)
- (void)mw_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MWKeyValueObservingOptions)options context:(nullable void *)context;
- (void)mw_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)mw_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey.id> *)change context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
Copy the code
#import "NSObject+KVO.h"
#import <objc/message.h>

static NSString *const kMWKVOPrefix = @"MWKVONotifying_";
static NSString *const kMWKVOAssiociateKey = @"kMWKVO_AssiociateKey";

@implementation NSObject (KVO)
- (void)mw_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MWKeyValueObservingOptions)options context:(nullable void *)context
{
    //1. Do not observe member variables, only observe attributes, by whether there is a setter method
    [self judgeSetterMethodFromKeyPath:keyPath];
    //2. Dynamically generate subclasses
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3. Isa points to the newly generated subclass
    object_setClass(self, newClass);
    //4. Save observer information
    MWKVOInfo *info = [[MWKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
    if(! observerArr) { observerArr = [NSMutableArray arrayWithCapacity:1];
        [observerArr addObject:info];
        objc_setAssociatedObject(self, (__bridge const void* _Nonnull)(kMWKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }} - (void)mw_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
    if (observerArr.count<=0) {
        return;
    }
    for (MWKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
           break; }}if (observerArr.count<=0) {
        // refer back to the parent class
        Class superClass = [self class];
        object_setClass(self, superClass); }}#pragmaMark **- Dynamically generate subclasses **
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@ % @ % @ "",kMWKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // Prevent repeated creation of new classes
    if (newClass) return newClass;
    /** * If memory is not present, create generate * parameter 1: parent class * parameter 2: name of the new class * parameter 3: extra space created by the new class */
    // 2.1: Application class
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2: Registration class
    objc_registerClassPair(newClass);
    // 2.3.1: Add class: the class refers to the class of the observed object
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)mw_class, classTypes);
    // 2.3.2: Add setters
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)mw_setter, setterTypes);
    return newClass;
}
Class mw_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
static void mw_setter(id self,SEL _cmd,id newValue){
    // 4: message forwarding: forward to the parent class
    // Change the value of the parent class - can be cast
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue       = [self valueForKey:keyPath];
    void (*mw_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... * /
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),};//objc_msgSendSuper(&superStruct,_cmd,newValue)
    mw_msgSendSuper(&superStruct,_cmd,newValue);
    // 1: Get the observer
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
    for (MWKVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0.0), ^ {NSMutableDictionary<NSKeyValueChangeKey.id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                // New and old values are processed
                if (info.options & MWKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & MWKeyValueObservingOptionOld) {
                    [change setObject:@ "" forKey:NSKeyValueChangeOldKey];
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey]; }}// 2: The message is sent to the observer
                SEL observerSEL = @selector(mw_observeValueForKeyPath:ofObject:change:context:);       objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL); }); }}} - (void)mw_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey.id> *)change context:(nullable void *)context
{ 
}
#pragmaMark **- Verifies the existence of setter methods **
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
    Class superClass    = object_getClass(self);
    SEL setterSeletor   = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if(! setterMethod) {@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@" Old iron has no setter for current %@",keyPath] userInfo:nil]; }}#pragmaMark **- Gets the name of the set method from the get method key ===>>> setKey:**
static NSString *setterForGetter(NSString *getter) {if (getter.length <= 0) { return nil; }NSString *firstString = [[getter substringToIndex:1] uppercaseString];
    NSString *leaveString = [getter substringFromIndex:1];
    return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
#pragmaMark **- Gets the name set of the getter method from the set method<Key>:===> key**
static NSString *getterForSetter(NSString *setter) {if (setter.length <= 0| |! [setter hasPrefix:@"set") | |! [setter hasSuffix:@ ","]) { return nil; }NSRange range = NSMakeRange(3.setter.length4 -);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0.1) withString:firstString];
}
@end
Copy the code