基于系统派生类自定义无冲突KVO

426 阅读2分钟

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

  • 创建派生子类 NSKVONotifying_Person
  • 修改被观察对象 p 的 isa 指针,使其指向新类 NSKVONotifying_Person
  • 重写 setter 方法,赋值并且通知观察者 observer 对象 p 的属性值发生了改变

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可以同时使用,无冲突。