Realize KVO (block) by yourself

Time:2022-5-16

Salute the pioneers first[https://tech.glowing.com/cn/implement-kvo/]

Read my previous imitation system to realize KVORealize KVO by yourself (agent)Students, it shouldn’t be too hard to see this, but there are several optimization points and problems to pay attention to

  • It is considered that multiple objects listen to the same object direction (multiple subclasses cannot be created, and the attribute monitored by one cannot repeat the association method)
  • Consider multiple objects listening to different objects of the same class
  • There is an episode when removing the listener (that is, the observer weak reference modified with weak is set to nil in advance, so it is impossible to compare and remove the corresponding listener)

The following is my specific analysis on the former
It consists of two partskvo_ Block classificationandLxcobservationinfo block save object
kvo_ Block classification

//
//  NSObject+kvo_block.m
//  leetCode
//
//Created by Liu Xiaochen on July 13, 2021
//

#import "NSObject+kvo_block.h"

static NSString *kvo_class_prefix = @"KVOClass_";
const void *kvo_observers = &kvo_observers;

@implementation NSObject (kvo_block)

- (void)lxc_addObserver:(NSObject *)observer forKey:(NSString *)key block:(AddObserverBlock)block {
    
    // 1. Determine whether the attribute exists
    SEL setterSelector = NSSelectorFromString(setterFromGetter(key));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        //Report exceptions to facilitate problem finding
        Nsstring * reason = [nsstring stringwithformat: @ "% @ No setter corresponding to% @", self, key];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                       reason:reason
                                     userInfo:nil];
    }
    
    //Primitive class
    Class oldClass = object_getClass(self);
    //Get the original class name
    NSString *oldClassName = NSStringFromClass(oldClass);
    
    //The judgment here is mainly to judge whether the current monitored object has been monitored. If it has been monitored, the class name will be prefixed with KVO. There is no need to re create a subclass and use the current class
    if (![oldClassName hasPrefix:kvo_class_prefix]) {
        Class kvoClass =  [self makeKvoClassWithOriginalClassName:oldClassName];
        // 3. Modify the ISA pointer so that self - > isa points to subclasses
        object_setClass(self, kvoClass);
    }
    
    //Determine whether the attribute is associated with KVO_ Setter, such as name, needs sel and KVO to listen to name for the first time_ Setter Association, and the second listening does not need to be associated again, but it is actually found that there is no problem with repeated addition
    IMP imp = class_getMethodImplementation(object_getClass(self), setterSelector);
    if (imp != (IMP)kvo_setter) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(object_getClass(self), setterSelector, (IMP)kvo_setter, types);
    }
    
    // 5. Save block and the parameters key and observer used for filtering (these two parameters are mainly used for filtering during remove)
    LXCObservationInfo *info = [[LXCObservationInfo alloc] init];
    info.block = block;
    info.key = key;
    info.observer = observer;
    
    
    //The reason for creating an array here is that the same object may be monitored in multiple places. At this time, you need to execute multiple blocks
    NSMutableArray *observers = objc_getAssociatedObject(self, kvo_observers);
    if (!observers) {
        //Create an array of listening objects
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, kvo_observers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
}

-(void)lxc_removeObserver:(NSObject *)observer forKey:(NSString *)key {
    NSMutableArray *observers = objc_getAssociatedObject(self, kvo_observers);
    for (LXCObservationInfo *observerInfo in observers) {
        /*
         Here, judge if the listening objects are consistent, the listening attributes are consistent, and delete
         Here is a problem that subverts my cognition. If I use weak to modify observer in info, here is observer info The observer cannot be obtained, because this method is usually called in dealloc, so I think it's incredible that all weak reference pointers pointing to themselves are set to nil before dealloc. But practice has proved that it is true, so I modify it with assign in info, because the externally transmitted observer has value at this time, so the assign modification will not be a problem
         */
        if (observerInfo.observer == observer && [observerInfo.key isEqualToString:key]) {
            [observers removeObject:observerInfo];
            break;
        }
    }
}

//Create subclasses
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName {
    NSString *kvoClazzName = [kvo_class_prefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);
    
    if (clazz) {
        return clazz;
    }
    
    Class originalClazz = object_getClass(self);
    
    //Let the new class inherit from the original class
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
    
    //Imitation Apple hidden subclass (and class object method overriding this class)
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
    
    //Register subclasses
    objc_registerClassPair(kvoClazz);
    
    return kvoClazz;
}

//Override class method
Class kvo_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}

//Unified management setter method
void kvo_setter(id self, SEL _cmd, id newName)
{
    
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterFromSetter(setterName);
    
    id  oldValue;
    if (getterName) {
        oldValue = [self valueForKey:getterName];
    }
    //Get KVO type
    id class = object_getClass(self);
    
    //Call the method of the parent class (another way here is to modify self isa to point to the original class, and then modify it into a subclass. Here is the way the system implements super. By the way, you can understand the difference between super and self)
    Class super_class = class_getSuperclass(class);
    struct objc_super * _Nonnull super_struct = malloc(sizeof(struct objc_super));
    super_struct->receiver = self;
    super_struct->super_class = super_class;
    objc_msgSendSuper(super_struct, _cmd,newName);
    
    id  newValue = [self valueForKey:getterName];
    
    NSMutableArray *observers = objc_getAssociatedObject(self, kvo_observers);
    for (LXCObservationInfo *observer in observers) {
        if ([observer.key isEqualToString:getterName]) {
            observer.block(oldValue, newValue);
        }
    }
}

//Get setter string through property
NSString* setterFromGetter(NSString *key) {
    if (key.length > 0) {
        NSString *resultString = [key stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] capitalizedString]];
        return [NSString stringWithFormat:@"set%@:",resultString];
    }
    return nil;
}

//Get getter through setter
NSString* getterFromSetter(NSString *key) {
    if (key.length > 0) {
        NSString *resultString = [key substringFromIndex:3];
        resultString = [resultString substringToIndex:resultString.length - 1];
        return [resultString lowercaseString];
    }
    return nil;
}

@end

Lxcobservationinfo block save object

//
//  LXCObservationInfo.h
//  leetCode
//
//Created by Liu Xiaochen on 2021 / 7 / 9
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void(^AddObserverBlock)(id oldValue, id newValue);

@interface LXCObservationInfo : NSObject

@property (nonatomic,assign)id observer;
@property (nonatomic,copy)NSString *key;
@property (nonatomic,copy)AddObserverBlock block;

@end

NS_ASSUME_NONNULL_END

The last is the verification of the episode
A weakly refers to B, and B implements block (print the B weakly referenced by a). Call block in B’s dealloc and find that the block is executed. The B weakly referenced by a is nil

A medium

@property (nonatomic,weak)TwoViewController *two;
self.two = vc;
__weak typeof(self) weakSelf = self;
vc.block = ^{
        NSLog(@"twoblock%@",weakSelf.two);
};

B medium

-(void)dealloc {
    self.block();
}

Final printing

2021-07-13 18:52:28.715064+0800 leetCode[74996:13935331] twoblock(null)

OK, that’s the end of the content about KVO. Because of the problem cited by break, I will update the knowledge points of a version of break later. I hope you can give me more support