Custom KVO, support multi-attribute listening, support automatic release.

Use the system KVO listen attribute

Let’s review how system KVO works:

@property (nonatomic.copy) NSString *msg;
@property (nonatomic.strong) Person *person;
Copy the code

1. Add observers

[self addObserver:self forKeyPath:@"msg" options:NSKeyValueObservingOptionNew context:nil];
[self addObserver:self forKeyPath:@"person.name" options:NSKeyValueObservingOptionNew context:nil];
Copy the code

2. Handle callbacks

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey.id> *)change context:(void *)context {
    
    if ([keyPath isEqualToString:@"msg"]) {
        NSLog(@" MSG new value: %@".self.msg);
    }
    if ([keyPath isEqualToString:@"person.name"]) {
        NSLog(@"person.name "new value: %@".self.person.name); }}Copy the code

3. Manually remove observers

[self removeObserver:self forKeyPath:@"msg"];
[self removeObserver:self forKeyPath:@"person.name"];
Copy the code

System KVO needs to write a lot of code, it needs to be manually released, and we need to decide which attribute to listen to, we automatically handle this process with custom KVO.

Use custom EasyKVO listening properties

KVO would certainly be much easier to use if it could be called in the following way:

Listen to MSG properties

[self observeProperty:@"msg" changedBlock:^(id newValue, id oldValue) {
    NSLog(@" > MSG: %@, %@", oldValue, newValue);
}];
Copy the code

Listen for the person.name property

[self.person observeProperty:@"name" changedBlock:^(id newValue, id oldValue) {
    NSLog(@" > person. Name: old value %@, new value %@", oldValue, newValue);
}];
Copy the code

So, we want our custom KVO to be easy to use without having to manually release it, without having to add a lot of if judgment to the callback function to distinguish different attributes.

Introduction to the main principles of custom KVO

Create a custom derived class

Create an intermediate class to mount the setter and dealloc methods:

NSString *oldClassName = NSStringFromClass([self class]);
NSString *pairClassName = [EASY_KVO_PREFIX stringByAppendingString:oldClassName];
Class pairClass = NSClassFromString(pairClassName);
pairClass = objc_allocateClassPair([self class], pairClassName.UTF8String, 0x68);
objc_registerClassPair(pairClass);
Copy the code

2. Add an automatic release method

If dealloc is not available, add the dealloc method. If so, swap dealloc implementations.

if(! _containSelector(self.@"dealloc")) {
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char * deallocType = method_getTypeEncoding(deallocMethod);
    class_addMethod(pairClass, deallocSEL, (IMP)easy_dealloc, deallocType);
} else {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method m1 = class_getInstanceMethod([self class].NSSelectorFromString(@"dealloc"));
        Method m2 = class_getInstanceMethod([self class].@selector(selEasyDealloc));
        method_exchangeImplementations(m1, m2);
    });
}
Copy the code

Dealloc implementation. Here I use the method of traversing all the property variables and setting null to avoid the problem of multiple property listening that can not release all the properties.

- (void)selEasyDealloc {
    __easy_dealloc(self);
    [self selEasyDealloc];
}

void easy_dealloc(id self, SEL _cmd) {
    __easy_dealloc(self);
}

void __easy_dealloc(id self) {
    NSLog(@"-- easy_dealloc %@".self);
    NSMutableDictionary *tips = _globalTipsMap();
    [tips removeAllObjects];
    Class oldClass = [self class];
    object_setClass(self, oldClass); /* Change back to the isa pointer */
    
    /* Release all attributes and variables */
    unsigned int count = 0;
    Ivar * ivarList = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        const char * name = ivar_getName(ivar);
        NSString * KEY = [NSString stringWithUTF8String:name];
        [self setValue:nil forKey:KEY];
        //NSLog(@"prop %@ released", KEY);}}Copy the code

Override the setter method

SEL setSel = NSSelectorFromString(_setterForProperty(keyPath));
Method setMethod = class_getInstanceMethod([self class], setSel);
const char * setType = method_getTypeEncoding(setMethod);
class_addMethod(pairClass, setSel, (IMP)easy_setter, setType);
Copy the code

The setter implementation, overrides the setter method, not only implements the original setter, but also calls the block callback out.

void easy_setter(id self, SEL _cmd, id newValue) {
    NSString *setterName = NSStringFromSelector(_cmd);
    if (setterName.length < 4) return;
    
    NSString *format = [setterName substringWithRange:NSMakeRange(3, setterName.length - 4)];
    NSString *keyPath = [format stringByReplacingCharactersInRange:NSMakeRange(0.1) withString:[[format substringToIndex:1] lowercaseString]];
    if (keyPath.length < 1) return;
    
    id oldValue = [self valueForKeyPath:keyPath];
    if(! [oldValue isEqual:newValue]) {// Call the parent setter
        struct objc_super superClass = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))};void (* msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
        msgSendSuper(&superClass, _cmd, newValue);
    }
    
    / / callback block
    NSString *KEY = [NSString stringWithFormat:@"_%@_%@_block".NSStringFromClass(object_getClass(self)), keyPath];
    NSMutableDictionary * tipMap = _globalTipsMap();
    EasyKVOChangedBlock block = (EasyKVOChangedBlock)[tipMap objectForKey:KEY];
    if(block) { block(newValue, oldValue); }}Copy the code

4. Modify the ISA pointer

Points isa to a custom derived class

object_setClass(self, pairClass);
Copy the code

5. Save the block callback information

Save the block information to call back when iterating the setter

NSString * KEY = [NSString stringWithFormat:@"_%@_%@_block".NSStringFromClass(pairClass), keyPath];
NSMutableDictionary *tips = _globalTipsMap();
if (block) {
    [tips setObject:[block copy] forKey:KEY];
} else {
    [tips setObject:[^{} copy] forKey:KEY];
}
Copy the code

Matters needing attention

  1. The following listening modes are not supported:
[self observeProperty:@"person.name" changedBlock:^(id newValue, id oldValue) {
   NSLog(@" > person. Name: old value %@, new value %@", oldValue, newValue);
}];
Copy the code
  1. Please do not use it with system KVO, it will cause conflict due to the ISA pointer.

Demo

Demo address: github.com/jerodji/Eas…