KVO of iOS underlying exploration - Custom KVO

Posted by wiseoleweazel on Sun, 02 Jan 2022 20:49:54 +0100

review

In the first two blogs, we have introduced the relevant operations of KVO, and the underlying logic of KVO is realized by dynamically generating subclasses and rewriting parent classes. How can we customize a KVO?

KVO (I) - An Introduction to KVO
KVO (II) - KVO principle analysis of iOS underlying exploration

1. Preliminary analysis

KVO of the system expands some capabilities on NSObject, as shown in the following figure:

The trilogy used by KVO of the system is:

  • Add listening addObserver
  • Listen for callback observeValueForKeyPath
  • Remove listener removeObserver

We also customize a KVO based on the API of the system, as follows:

@interface NSObject (JP_KVO)

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

We customize KVO to observe attributes, so there is no setter method for member variables, so the first step is to filter out member variables. How to dynamically generate subclasses, change the direction of isa and save observers, the steps are as follows:

  1. Verify whether there is a setter method: do not let member variables (instance variables) in
  2. Dynamically generate subclasses
  3. Change the direction of subclass isa
  4. Save our observers
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    
    // 1: Verify whether there is a setter method: do not let the instance in
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: Dynamically generate subclasses
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: Direction of ISA
    object_setClass(self, newClass);
    // 4: Save observer
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(KJPKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

2. Verify whether setter method exists

  • judgeSetterMethodFromKeyPath:keyPath
- (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:@"Sorry, there is no current%@of setter",keyPath] userInfo:nil];
    }
}

Judge whether there is a setter method, and throw exception information if there is no setter method

3. Dynamically generate subclasses

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kjpKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // Prevent duplicate creation and generation of new classes
    if (newClass) return newClass;
    /**
     * If memory does not exist, create a build
     * Parameter 1: parent class
     * Parameter 2: name of the new class
     * Parameter 3: additional space for the development of new classes
     */
    // 2.1: Application
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2: Registration
    objc_registerClassPair(newClass);
    // 2.3. 1: Add Class: the class points to JPPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)jp_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)jp_setter, setterTypes);
    return newClass;
}
  • JP_class returns parent class information
Class JP_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
  • Judge whether the subclass has been created, and create it if not
  • Application category
  • Registration class
  • If the newClass does not exist, call objc_allocateClassPair creates a kvo subclass and overrides the - class method
  • Add setter
  • Return newClass

4. Create setter

Creating setter method is mainly divided into two parts: calling parent class method and sending notification

static void jp_setter(id self,SEL _cmd,id newValue){
    // 4: Message forwarding: forward to parent class
    // Change the value of the parent class - you can cast
    void (*jp_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)
    jp_msgSendSuper(&superStruct,_cmd,newValue);
    
    // Now that we have observed it, the next step is not to call back -- let our observer call
    // - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    // 1: Get the observer
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey));
    
    // 2: The message is sent to the observer
    SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
}

  • You can call the parent class method through objc_msgSendSuper implementation, call the setter of the parent class (you can also call it through performSelector

  • Notify the observer that the keypath can be accessed through_ cmd conversion, object is self, change can also be obtained, and context can not be transmitted first. So the core is the acquisition of observer.

  • The observer is stored by associating objects

  • When you get the observer, you send the message to the observer for information callback processing

  • getterForSetter

#pragma mark - get the name of getter method from set method set < key >: = = = = > key
static NSString *getterForSetter(NSString *setter){
    
    if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
    
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *getter = [setter substringWithRange:range];
    NSString *firstString = [[getter substringToIndex:1] lowercaseString];
    return  [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}

More content is constantly updated

🌹 Just like it 👍🌹

🌹 If you think you have something to gain, you can have a wave, collect + pay attention, comment + forward, so as not to find me next time 😁🌹

🌹 Welcome to exchange messages, criticize and correct each other 😁, Self improvement 🌹

Topics: iOS