乐趣区

关于ios:基于系统派生类自定义KVO

作为 iOSer,想必大家对 KVO 并不生疏,其原理概括起来大抵 3 个步骤:

  • 创立派生子类 NSKVONotifying_Person
  • 批改被察看对象 p 的 isa 指针,使其指向新类 NSKVONotifying_Person
  • 重写 setter 办法,赋值并且告诉观察者 observer 对象 p 的属性值产生了扭转

然而,零碎的 KVO 着实不好用,察看多个属性时,须要在 observeValueForKeyPath:ofObject:change:context: 中写上大量的判断条件,于是,基于以上的 KVO 实现原理咱们能够自定义 KVO 实现。

那么,如何自定义 KVO?

基于自定义派生类的 KVO

零碎办法的派生类 NSKVONotifying_Person,咱们绕过这个零碎派生类,自定义一个派生类,比方叫做 CustomKVO_Person,应用其替换 NSKVONotifying_Person。

这种形式在自定义的办法中是可行的,然而一旦和零碎的 addObserver:forKeyPath:options:context: 一起应用就会 crash;

其解决方案也是有的,在 iOS 大解密:玄之又玄的 KVO 一文中,给出的解决方案是:

  • 给自定义派生类调配 0x68 空间,拷贝零碎派生类的 indexedIvars 到此空间,保障 setter 时 _NSSetIntValueAndNotify 能正确获取 KVO 信息,防止其 crash;
  • 借助 FishHook 来 hook 零碎的 object_setClass 操作,判断 isa 指针为自定义派生类且继承于零碎派生类时跳过 setClass 操作,这样一来,即便调用零碎办法也能保障 isa 指针指向自定义派生类,防止自定义 KVO 生效;

以上两步联合就能够解决自定义派生类 KVO 与零碎办法混用导致的问题了。

然而,如果仅是自定义一个 KVO 就要引入 FishHook 的话,这感觉可不太好;那么有没有不必引入任何框架,也能实现 KVO 并且不与零碎抵触的办法呢?

上面,咱们来看看另一种思路

基于零碎派生类自定义的 KVO

这种思路不须要创立自定义的派生类,代码实现上与自定义派生类 KVO 大同小异,先调用零碎办法生成零碎派生类,再批改零碎派生类的 setter 办法 IMP,调用咱们自定义的 block 回调。

代码实现如下:

#import "NSObject+EasyKVO.h"
#import <objc/message.h>
#import "MRCEasyKVOTools.h"

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

static NSString * __EasyKVOTipsDic = @"__EasyKVOTipsDic";

@implementation NSObject (EasyKVO)

#pragma mark - public

- (void)addObserver:(NSObject*)observer forKeyPath:(NSString *)keyPath changedBlock:(_EasyKVOChangedBlock)block
{if (!observer || keyPath.length < 1) return;

    /* 应用零碎办法取得派生类 */
    if (![NSStringFromClass(object_getClass(self)) containsString:@"NSKVONotifying_"]) {[self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    }
    NSString *pairClsName = NSStringFromClass(object_getClass(self));
    Class pairCls = NSClassFromString(pairClsName);
    if (!pairCls) {pairCls = objc_allocateClassPair(object_getClass(self), pairClsName.UTF8String, 0x68);
        [MRCEasyKVOTools object_copyIndexedIvars:object_getClass(self) toTarget:pairCls size:0x68];
        objc_registerClassPair(pairCls);
        object_setClass(self, pairCls); /* 批改 isa 指针 */
    }
    
    /* 保留 block 信息 */
    [_tipsMap(self, _cmd) setObject:[block copy] forKey:[NSString stringWithFormat:@"_%@_%@_block", NSStringFromClass(object_getClass(self)), keyPath]];
    
    /*  扭转 setter 办法 */
    NSString *format = [keyPath stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[keyPath substringToIndex:1] uppercaseString]];
    SEL setSel = NSSelectorFromString([NSString stringWithFormat:@"set%@:", format]);
    Method setMethod = class_getInstanceMethod(object_getClass(self), setSel);
    if (![self _containSelector:setSel]) { /* 避免增加屡次 */
        class_addMethod(object_getClass(self), setSel, (IMP)_setterFunction, method_getTypeEncoding(setMethod));
    } else {class_replaceMethod(object_getClass(self), setSel, (IMP)_setterFunction, method_getTypeEncoding(setMethod));
    }
}

- (void)removeObserver:(NSObject *)observer blockForKeyPath:(NSString *)keyPath {[self removeObserver:observer forKeyPath:keyPath];
    NSString *blockKeyName = [NSString stringWithFormat:@"_%@_%@_block", @"NSKVONotifying_", keyPath];
    NSMutableDictionary *tips = _tipsMap(self, _cmd);
    [tips removeObjectForKey:blockKeyName];
}

#pragma mark - private

void _setterFunction(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]) {
        // 调用父类 setter
        struct objc_super supercls = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))
        };  
        void (* msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
        msgSendSuper(&supercls, _cmd, newValue);
    }     
    
    _EasyKVOChangedBlock block = (_EasyKVOChangedBlock)[_tipsMap(self, _cmd) objectForKey:[NSString stringWithFormat:@"_%@_%@_block", NSStringFromClass(object_getClass(self)), keyPath]];    
    if (block) block(newValue, oldValue);
}

NSMutableDictionary *_tipsMap(id self, SEL _cmd) {NSMutableDictionary * _tipsDic = objc_getAssociatedObject(self, &__EasyKVOTipsDic);
    if (!_tipsDic) {_tipsDic = [[NSMutableDictionary alloc] init];
        objc_setAssociatedObject(self, &__EasyKVOTipsDic, _tipsDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return _tipsDic;
}

- (BOOL)_containSelector:(SEL)selector {Class cls = object_getClass(self);
    unsigned int count = 0;
    Method *methods = class_copyMethodList(cls, &count);
    for (int i=0; i<count; i++) {SEL sel = method_getName(methods[i]);
        if (selector == sel) {free(methods);
            return YES;
        }
    }
    free(methods);
    return NO;
}

@end

其中 MRCEasyKVOTools 用到了 MRC 环境下的 API:

/*
 只能在 MRC 环境编译
 Build Phases 中 MRCEasyKVOTools.m 增加 -fno-objc-arc  
 */

#import "MRCEasyKVOTools.h"
#import <objc/objc.h>

@implementation MRCEasyKVOTools

+ (void)object_copyIndexedIvars:(id)obj toTarget:(id)targetObj size:(size_t)size 
{uint64_t *s1 = object_getIndexedIvars(obj);
    uint64_t *s2 = object_getIndexedIvars(targetObj);
    memcpy(s2, s1, size);
}

@end

通过测试,零碎 KVO 与自定义 KVO 能够同时应用,无抵触。

退出移动版