KVO exploration and implementation of custom KVO

 

1. What is KVO

KVO Apple Official Documentation

KVO(Key-Value Observing): Objective-C's implementation of the Observer design pattern. Key-Value Observing is a mechanism that allows objects to be notified when specified properties of other objects change.

2. The principle of the underlying implementation of KVO

Key-Value Observing Implementation Details (official document)

Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch t able essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

    1. KVO is implemented based on the isa-swizzling technology implemented by runtime
    2. When an object is registered as an observer, the system will dynamically create a derived class NSKVONotifying_XX of the class at runtime, and the isa pointer of the object is modified to point to the derived class instead of the real class
    3. Override the setter method of the observed property in this derived class, and the derived class implements the real notification mechanism in the overridden setter method
    4. The notification of KVO is mainly based on the two methods willChangeValueForKey and didChangevlueForKey. This method is called before the value of willChangeValueForKey is changed, and the oldValue is recorded. After the change, the didChangevlueForKey method is called, and then the observeValueForKey:ofObject:change:context: method is called
    5. The KVO implementation also rewrites the class to hide the derived class

3. Demonstration of the underlying implementation of KVO

3.1 Prepare the code

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [Person new];
    //context:(nullable void *) void * type use NULL
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];

    self.person.name = @"+";
}

- (IBAction)modifiedValue:(UIButton *)sender {
    self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
}

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

- (IBAction)removeObserve:(UIButton *)sender {
    [self.person removeObserver:self forKeyPath:@"name"];
    NSLog(@"Observer removed successfully");
}

Results of the:

listening method:{
    kind = 1;
    new = "+";
    old = "<null>";
}
listening method:{
    kind = 1;
    new = "++";
    old = "+";
}
listening method:{
    kind = 1;
    new = "+++";
    old = "++";
}
  • kind = 1 By looking at the enumeration definition, we can see that NSKeyValueChangeSetting belongs to the value modified by the setter method
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,     //property setter methods
    NSKeyValueChangeInsertion = 2,   //How to add a collection type
    NSKeyValueChangeRemoval = 3,     //Collection removal
    NSKeyValueChangeReplacement = 4, //Element replacement for mutable collection types
};

3.2 Verify the derived class NSKVONotifying_XX

  • At breakpoint 1, we print the class name of the object and get Person
  • At breakpoint 2, we print the class name of the object and get NSKVONotifying_Person
  • This verifies that a derived class will be generated after adding the KVO bottom layer,

3.3 Verify willChangeValueForKey and didChangevlueForKey

Prepare the code:

// Turn off automatic value creation observers
+ (BOOL)accessInstanceVariablesDirectly:(NSString *)key {
    return NO;
}

- (void)setName:(NSString *)name {
    [self willChangeValueForKey:name];
    NSLog(@"setter method: willChangeValueForKey -%@", _name);
    _name = [name copy];
    [self didChangeValueForKey:name];
    NSLog(@"setter method: didChangeValueForKey -%@", _name);
}

Results of the:

setter method: willChangeValueForKey -(null)
setter method: didChangeValueForKey -+
listening method:{
    kind = 1;
    new = "+";
    old = "<null>";
}
setter method: willChangeValueForKey -+
setter method: didChangeValueForKey -++
listening method:{
    kind = 1;
    new = "++";
    old = "+";
}

It perfectly verifies our argument above

3.4 After removing the notification, isa points to the original class

From the above figure, it can be concluded that after KVO is removed, isa points to the original class again

4. Use of KVO

Apple Documentation - Mutable Arrays

In the official KVC documentation, the collection type for mutable arrays needs to pass the mutableArrayValueForKey method so that elements can be added to the mutable array

self.person.dataArray = [NSMutableArray array];
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"1"];

4.2 KVO: One-to-many observation

- (NSString *)downloadProgress {
    return [NSString stringWithFormat:@"%f", 1.0f * self.writeData / self.totalData];
}

// path processing
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSMutableSet *keyPaths = [[super keyPathsForValuesAffectingValueForKey:key] mutableCopy];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKey = @[@"totalData", @"writeData"];
        [keyPaths setByAddingObjectsFromArray:affectingKey];
    }
    return keyPaths;
}


//2. Register for KVO observation
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];


//3. Trigger attribute value changes
self.person.totalData += 1;
self.person.writeData += 1;

//4. Remove the observer
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"downloadProgress"];
}

5. Implementation of custom KVO

5.1 Registering Observers

1. Verify that the current keyPath has a setter method

- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath {
    Class superClass = object_getClass(self);

    NSString *setterMethodName = setterForGetter(keyPath);
    SEL setterSEL = NSSelectorFromString(setterMethodName);
    Method setterMethod = class_getInstanceMethod(superClass, setterSEL);

    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"No%@of set method", keyPath] userInfo:nil];
    }
}

2. Dynamically generate derived subclass KVONotifying_xxx

- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@", kKVOPrefix, oldClassName];
    Class newClass = NSClassFromString(newClassName);
    if (newClass) return newClass;
    //2.1 Application class
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    //2.2 Registration
    objc_registerClassPair(newClass);
    //2.3 Add method attribute -ivar -ro

    // 2.3.1 : Add class : The point of class is Person
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)kvo_class, classTypes);
    // 2.3.2 : Add setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)kvo_setter, setterTypes);

    return newClass;
}

3. Modify the isa point to point to the subclass

object_setClass(self, newClass);

4. Save information

    KVOInfo *info = [[KVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
  • Full registration method code
- (void)kvo_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KVOBlock)block {
    //1. Verify that the setter exists
    [self judgeSetterMethodFromKeyPath:keyPath];
    //2. Dynamic generation, etc.
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    //3. Modify isa to point to KVONotifying_XX
    object_setClass(self, newClass);

    //4. Save information
    KVOInfo *info = [[KVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

5.2 Responders

  • In the setter method, convert the system objc_msgSendSuper into a custom message to send
  • Tell the observer to respond with a block
static void kvo_setter(id self, SEL _cmd, id newValue)
{
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    //  Message forwarding: forward to the parent class
    // Change the value of the parent class --- you can cast the type
    void (*kvo_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)
    kvo_msgSendSuper(&superStruct, _cmd, newValue);

    // Information data callback
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey));

    for (KVOInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

 

5.3 Remove the observer

- (void)kvo_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey));
    if (observerArr.count <= 0) {
        return;
    }

    for (KVOInfo *info in observerArr) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [observerArr removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            break;
        }
    }

    if (observerArr.count <= 0) {
        // refer back to the parent class
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

See the complete code GitHub->KVOExplore

References:

FBKVOController Apple Documentation

Tags: iOS

Posted by gevo12321 on Sun, 08 May 2022 23:10:04 +0300