Study harmoniously! Don’t be impatient!! I’m your old friend, Xiao Qinglong

preface

The previous article explored and analyzed the principle of KVO, this article carries out a custom definition of KVO on the principle content. First of all, we will sort out the customized things to do based on the principle:

  • Have a set method or not (KVO is essentially an observation of a set method)

  • Dynamically generate a subclass and add a set method

  • The isa of the instance variable of the original class points to the subclass

  • The set method is called to initiate the notification

  • After remove, modify the instance variable’s ISA to point to the original class

Core method

Add observation mode

// Add observation mode:
- (void)ssj_addObserver:(id)observer forKeyPath:(NSString *)keyPath observerType:(ObserverType)observerType context:(NSString *)context{
    
    // Check if you have a set method, (" member variable "is not observed by KVO, we have already tested in KVO principle)
    [self hasSetterMethod:keyPath];
    
    /// Record this observation
    [self addKVOInfo:observer keyPath:keyPath context:context observerType:observerType];
    
    // create the SSJNSKVONotifying_xxx subclass
   Class subCls = [self ssj_createSubClassFromOriginClass:nil forKeyPath:keyPath];
    
    /// the isa of the instance variable points to the subclass
    object_setClass(self, subCls);
}
Copy the code

Remove the specified observation mode:

/// Remove the specified observation mode
/// @param observer
// @param keyPath View key path
- (void)ssj_removeObserver:(id)observer forKeyPath:(NSString *)keyPath{
    
    // 1. Remove the kVOInfo record from kVOInfos
    [self removeKVOInfo:observer keyPath:keyPath context:NULL];
    
    // 2. The isa of the instance variable refers to the original class
    NSArray *kVOInfos = objc_getAssociatedObject(self, SSJKVOInfoKey);
    if(kVOInfos.count == 0) {/// kvoinfos. count is empty, indicating that all observation modes have been removed, and the instance object's "isa" can point to the "original class".
        /// [self class] gets the original class
        object_setClass(self, [self class]); }}Copy the code

Notification callback (requires observer implementation):

/// notification callback
/// This method is notified when the value changes
- (void)ssj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    /// The observer needs to override this method
}
Copy the code

Dynamically generate a subclass

- (Class)ssj_createSubClassFromOriginClass:(Class)originClass forKeyPath:(NSString *)keyPath{

    NSString *originClassName = NSStringFromClass([self class]);
    NSString *ssj_subClassName = [@"SSJNSKVONotifying_" stringByAppendingString:originClassName];
    const char *ssj_subClassName_char = ssj_subClassName.UTF8String;
    Class class_new = objc_getClass(ssj_subClassName_char);
    // Determine if the subclass exists and create it if it does not
    if(! class_new){ NSLog(@"Create SSJNSKVONotifying_ subclass ~");
        / / / application class
        class_new = objc_allocateClassPair([self class].ssj_subClassName_char, 0);
        / / / registered classes
        objc_registerClassPair(class_new);
        // add methods (add all instance methods of the "original class" to the "SSJNSKVONotifying_ subclass")
        SEL sel_class = NSSelectorFromString(@"class");
        Method md = class_getInstanceMethod([self class].sel_class);
        const char*type = method_getTypeEncoding(md);
        class_addMethod(class_new, sel_class, (IMP)ssj_class, type);
        
        /// add the corresponding setter method
        if (keyPath.length > 0) {
            /// setKeypath
            NSString *method_setName = [NSString stringWithFormat:@"set%@:",[self smallToBigWithFirstCharFrom:keyPath]];
            SEL sel_keyPath = NSSelectorFromString(method_setName);
            Method md_keyPath = class_getInstanceMethod([self class].sel_keyPath);
            constchar*type_keyPath = method_getTypeEncoding(md_keyPath); class_addMethod(class_new, sel_keyPath, (IMP)ssj_keypathMethod, type_keyPath); }}return class_new;
}
Copy the code

Setter method for keypath

Pragma mark - setter methodstatic void ssj_keypathMethod(id self,SEL _cmd,id value_new){
    /// get the old value
    NSString *old_key = getterForSetter(NSStringFromSelector(_cmd));
    NSString *old_Value = nil;
    if (old_key && old_key.length > 0) {
        old_Value = [self valueForKey:old_key];
    }
    /// start calling the set:value: method of the parent class
    struct objc_super obj_super_class = {
       .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
   };
   // convert objc_msgSendSuper to the corresponding function pointer
   int (*ssj_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
   /// call the ssj_msgSendSuper method. The & operator is the c/ C ++ pointer operator
    ssj_msgSendSuper(&obj_super_class, _cmd, value_new); // The return value is an int based on the actual method called
    
    /// Print new and old values
    NSLog(@"SSJNSKVONotifying_XXX listens on set method -- old value: %@ - new value: %@",old_Value,value_new);
    
    // changeDic contains the returned values (new value, old value, etc., see observerType)
    NSMutableDictionary *changeDic = [NSMutableDictionary new];
    
    id obser = nil;
    NSArray *kVOInfos = objc_getAssociatedObject(self, SSJKVOInfoKey);
    /// The Array obtained by objc_getAssociatedObject is of Array type, which is converted to NSMutableArray
    NSMutableArray *kVOInfos_new = [kVOInfos mutableCopy];
    for (SSJKVOInfo *kVOInfo in kVOInfos) {
        // determine the keyPath match
        BOOL keyPathCompare = [kVOInfo.keyPath isEqualToString:old_key];
        
        /// match what data needs to be returned
        if (kVOInfo.observerType == ObserverTypeOfNewAndOld) {
            if (old_Value) {
                changeDic = [@{
                    @"value_old":old_Value,
                    @"value_new":value_new,
                } mutableCopy];
            }else{
                changeDic = [@{
                    @"value_new":value_new, } mutableCopy]; }}else if (kVOInfo.observerType == ObserverTypeOfNew) {
            changeDic = [@{
                @"value_new":value_new,
            } mutableCopy];
        }else if (kVOInfo.observerType == ObserverTypeOfOld) {
            changeDic = [@{
                @"value_old":old_Value,
            } mutableCopy];
        }
        // a callback is initiated if the match is successful
        if (keyPathCompare) {
            obser = kVOInfo.observer;
            if(! obser) {// the observer is released, but the data is still stored in kVOInfos. The garbage data should be deleted
                [kVOInfos_new removeObject:kVOInfo];
                continue;// Ignore the case where obser is null
            }
            
            /// Send a callback to the observerSEL ssj_obser_sel = @selector(ssj_observeValueForKeyPath:ofObject:change:context:); objc_msgSend(obser,ssj_obser_sel,old_key,self,changeDic,kVOInfo.context); }}/// Override assignment (now kVOInfo has removed the data record that obser is nil)
    objc_setAssociatedObject(self, SSJKVOInfoKey, kVOInfos_new, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
Copy the code

Helper methods, constant strings

Constant string
static const void *SSJKVOInfoKey = &SSJKVOInfoKey;

static NSString const* SSJClassHead = @"SSJNSKVONotifying_";
Copy the code
Whether a set method is available
// whether the set method is available
- (void)hasSetterMethod:(NSString *)keyPath{
    // get the full method name setXyy
    NSString *method_setName = [NSString stringWithFormat:@"set%@:",[self smallToBigWithFirstCharFrom:keyPath]];
    SEL sel_class = NSSelectorFromString(method_setName);
    Method md = class_getInstanceMethod([self class].sel_class);
    NSString *showMsg = [NSString stringWithFormat:@"Failed to add observation mode because %@ is not an attribute",keyPath];
    NSAssert(md, showMsg);
}

Copy the code
Returns parent class information
/// returns parent information
Class ssj_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
Copy the code
Gets the name of the getter method from the set method
/// @"setName:" -> @"name"
static inline NSString *getterForSetter(NSString *setter){
    NSString *strBegin = [setter substringToIndex:3];
    NSString *strEnd = [setter substringFromIndex:setter.length-1];
    /// start with "set" and end with ":"
    if ([strBegin isEqualToString:@"set"] && [strEnd isEqualToString:@":"]) {
        // remove "set" and ":"
        NSString *string_return = [setter substringFromIndex:3];
        string_return = [string_return substringToIndex:string_return.length-1];
        /// Convert the first character to lowercase
        string_return = bigToSmallWithFirstCharFrom(string_return);
        return string_return;
    }
    return nil;
}
Copy the code
The first letter is converted to uppercase
/// Capitalizes the first character of the string argument
/// @param string Specifies the character to be processed
/// return returns the processed string
- (NSString *)smallToBigWithFirstCharFrom:(NSString *)string{
    if (string) {
        if (string.length == 1) {
            return string.uppercaseString;/// convert to uppercase
        }else if (string.length > 1) {
            NSString *firstChar = [string substringToIndex:1];
            NSString *atferDeal = firstChar.uppercaseString;/// convert to uppercase
            NSString *endStr = [string substringFromIndex:1];
            return [NSString stringWithFormat:@"% @ % @",atferDeal,endStr];
        }else{
            return @"";
        }
    }
    NSLog(@"Parameter string is empty, conversion failed ~");
    return nil;
}
Copy the code
The first letter is converted to lowercase
/// Convert the first character of the string argument to lowercase
/// @param string Specifies the character to be processed
/// return returns the processed string
static inline NSString *bigToSmallWithFirstCharFrom(NSString *string){
    if (string) {
        if (string.length == 1) {
            return string.lowercaseString;/// convert to lowercase
        }else if (string.length > 1) {
            NSString *firstChar = [string substringToIndex:1];
            NSString *atferDeal = firstChar.lowercaseString;/// convert to lowercase
            NSString *endStr = [string substringFromIndex:1];
            return [NSString stringWithFormat:@"% @ % @",atferDeal,endStr];
        }else{
            return @"";
        }
    }
    NSLog(@"Parameter string is empty, conversion failed ~");
    return nil;
}
Copy the code
Added kVOInfos management
/** kVOInfos is an extended property for NSObject, of type NSMutableArray 
      
       * (note: the type obtained by objc_setAssociatedObject is immutable NSArray) Record the observed information, "ssj_addObserver:" or "ssj_removeObserver:" and "ssj_keypathMethod" may change kVOInfos, depending on the code. Since the "observer" property of "SSJKVOInfo" is "weak", it becomes nil when the observer is destroyed; As the keypath set method, ssj_keypathMethod determines whether the observer exists before sending a message to the observer. Therefore, even if "ssj_removeObserver:" is not called, the next use will not be affected. * /
      

/// Manually add the kvoinfos-get method
- (NSMutableArray<SSJKVOInfo *> *)kVOInfos{
    return objc_getAssociatedObject(self, SSJKVOInfoKey);
}

/// Manually add the kvoinfos-set method
- (void)setKVOInfos:(NSMutableArray<SSJKVOInfo *> *)kVOInfos {
    objc_setAssociatedObject(self, SSJKVOInfoKey, kVOInfos, OBJC_ASSOCIATION_COPY_NONATOMIC);
}


/// add - new kVOInfo
/// @param observer
// @param keyPath View key path
/// @param context
- (BOOL)addKVOInfo:(id)observer keyPath:(NSString *)keyPath context:(NSString *)context observerType:(ObserverType)observerType{
    NSArray *kVOInfos = objc_getAssociatedObject(self, SSJKVOInfoKey);
    NSMutableArray *kVOInfos_new = [kVOInfos mutableCopy];
    for (SSJKVOInfo *kVOInfo in kVOInfos) {
        BOOL observerCompare = kVOInfo.observer == observer;
        BOOL keyPathCompare = [kVOInfo.keyPath isEqualToString:keyPath];
        if (observerCompare && keyPathCompare) {
            /// indicates that the listener already exists
            return NO;
        }
    }
    
    SSJKVOInfo *kVOInfo_new = [SSJKVOInfo new];
    kVOInfo_new.observer = observer;
    kVOInfo_new.keyPath = keyPath;
    kVOInfo_new.context = context;
    kVOInfo_new.observerType = observerType;
    if(! kVOInfos_new) { kVOInfos_new = [NSMutableArraynew];
    }
    [kVOInfos_new addObject:kVOInfo_new];
    self.kVOInfos = kVOInfos_new;
    return YES;
}


/// remove - specify kVOInfo
/// @param observer
// @param keyPath View key path
/// @param context
- (void)removeKVOInfo:(id)observer keyPath:(NSString *)keyPath context:(nullable void *)context{
    NSMutableArray *kVOInfos_new = [self.kVOInfos mutableCopy];
    for (SSJKVOInfo *kVOInfo in self.kVOInfos) {
        BOOL observerCompare = kVOInfo.observer == observer;
        BOOL keyPathCompare = [kVOInfo.keyPath isEqualToString:keyPath];
        if (observerCompare && keyPathCompare) {
            /// indicates that the listener already exists
            [kVOInfos_new removeObject:kVOInfo];
            continue;
        }
        /// Since observer is weak, if the observer does not exist, then observer becomes nil
        /// The SSJKVOInfo data becomes dirty and needs to be cleaned up
        if(! kVOInfo.observer){ [kVOInfos_new removeObject:kVOInfo]; } } self.kVOInfos = kVOInfos_new; }Copy the code

How to use

- (void)viewDidLoad {
    [super viewDidLoad];
    
    SSJPerson *person = [SSJPerson shareSingleSSJPerson];
    person.name = @"Zhang";
    [person ssj_addObserver:self forKeyPath:@"name" observerType:ObserverTypeOfNewAndOld context:@"SSJPerson_name_Context"];
    person.name = @"Bill";
    NSString *str = person.name;
    NSLog(@"%s view current name:%@",__func__,str);
}

/// notification callback
- (void)ssj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(NSString *)context{
    NSLog(@"Callback ~ ~ ~ %@",change);
}
Copy the code

Operation effect:

At this point isa still points to the middle class:

After remove isa points to the original class:

To optimize the

So far, we know there are three steps to calling a custom KVO:

  1. ssj_addObserver:forKeyPath:observerType:context:

  2. ssj_observeValueForKeyPath:ofObject:change:context:

  3. ssj_removeObserver: forKeyPath:

So, is there a way to automate ssj_removeObserver? If it were you, what would you do? Yeah, I came up with a method swap. Normally, we would put the method exchange in + (void)load, which obviously doesn’t work because we don’t need to intercept all the “NSObject” class dealloc methods. Concrete implementation:

/// Add observation mode
- (Class)ssj_createSubClassFromOriginClass:(Class)originClass forKeyPath:(NSString *)keyPath{
    ...
    /// write it here instead of in the load method, only for observers who called ssj_addObserver
        Method originalMethod = class_getInstanceMethod([self class].NSSelectorFromString(@"dealloc"));
        Method swizzlingMethod = class_getInstanceMethod([self class], @selector(ssj_dealloc));
        method_exchangeImplementations(originalMethod, swizzlingMethod);
}

- (void)ssj_dealloc{
    NSLog(@"%@ dealloc I intercepted ~~~dealloc",NSStringFromClass([self class]));
    /// the isa of the instance variable points to the original class
    object_setClass(self, [self class]);
    /// clear the record
    self.kVOInfos = [NSMutableArray new];
}
Copy the code

Run:

There seems to be no problem, but tests show that:

  • The observer is set to the ViewContor class (and derived classes), which is fine whether the observer implements dealloc or not.

  • When the observer is set to the data model itself and the dealloc method is not implemented, the dealloc address will not be found.

SSJPersonClass observe themselvesweightProperty changes and is not implementeddeallocMethods:

Code to modify:

/// Add observation mode
- (Class)ssj_createSubClassFromOriginClass:(Class)originClass forKeyPath:(NSString *)keyPath{
    ...
    // add the dealloc method to the middle class, imp to ssj_dealloc
    Method originalMethod = class_getInstanceMethod([self class].NSSelectorFromString(@"dealloc"));
    const char * types = method_getTypeEncoding(originalMethod);
    class_addMethod(class_new, NSSelectorFromString(@"dealloc"), (IMP)ssj_dealloc,types);
}

static void ssj_dealloc(id self,SEL _cmd){
    NSLog(@"%@ dealloc I intercepted ~~~dealloc",NSStringFromClass([self class]));
    /// the isa of the instance variable points to the original class
    object_setClass(self, [self class]);
    /// clear the record
    objc_setAssociatedObject(self, SSJKVOInfoKey, [NSMutableArray new], OBJC_ASSOCIATION_COPY_NONATOMIC);
}
Copy the code

Run:

At this point, the automatic destruction function is completed.

code

Link: pan.baidu.com/s/1HtdFa240… Password: 547 z