iOS-自定义KVO

作者: xxxxxxxx_123 | 来源:发表于2020-02-26 20:46 被阅读0次

    KVO原理及使用

    我们之前讨论过KVO的原理,知道KVO机制是生成了一个中间类NSKVONotifying,该中间类是被观察对象的原类的子类,被观察对象的isa指针从指向原来的类改成指向新生成的中间类。而新生成的中间类,通过重写setter方法将重写的信息回调给观察者。

    那么根据这些信息,我们是否能够自定义一个KVO呢?答案是可以的。下面我们就简单的模拟一个KVO的实现。

    确定方法在NSObject的分类中实现

    通过官方实现的KVO可以看出,KVO是用NSObject的分类实现,其注册和移除方法是NSObjectNSKeyValueObserving分类,其回调方法是NSObjectNSKeyValueObserverRegistration分类。所以,我们要实现自定义,必须也是写一个NSObject的分类方法。

    @interface NSObject (TKVO)
    @end
    

    确定回调的信息

    官方的KVO分为三部曲: 添加观察者、回调更改、移除观察者。方法如下:

    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
    
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    

    其添加观察者、更改回调的方法是分离的,类似于代理,观察者实现了添加观察的方法,观察对象的更改通过另一个方法再传过来。这种传值方式我们可以使用delegateblocknotification等实现。为了让方法的使用更加的简洁,此处我们用block的回调实现。

    更改回调方法里主要的参数如下:

    • 观察对象
    • 观察路径,也就是属性
    • change也就是改变,包括旧值、新值等
    • context上下文指针

    我们可以定义一个block,将这些数据都放进block里进行回传。此处为了简单,就忽略掉context

    typedef void(^TObserveingBlock) (id observerObject, NSString *observeKeyPath, id oldValue, id newValue);
    

    确定注册方法名称、参数

    然后我们再来看看注册观察者的方法,其参数如下:

    • 观察者
    • 观察路径,属性
    • 设置回调的内容和时机
    • 上下文指针

    此处,我们只考虑最简单的情况,在回调的block返回新值、旧值,所以我们自定义的方法就不考虑NSKeyValueObservingOptionscontext了。

    至此,我们自定义的方法参数就确定了:

    • 观察则
    • 观察路径
    • 回调block

    方法如下:

    - (void)addTObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(TObserveingBlock)block;
    

    注册、回调方法的实现

    根据KVO的实现原理,我们要做以下处理:

      1. 因为KVO是基于KVC的,所以检查被观察对象的类是否有相应的setter方法,如果没有就抛出异常
      1. 检查被观察对象isa指向的类是不是一个KVO类,如果不是,新建一个继承于被观察对象的类的子类,并把isa指向这个新建的子类
      1. 检查这个中间类是否重写过要观察属性的setter方法,如果没有,添加重写的setter方法
      1. 添加观察者

    检查被观察的对象的类有没有相应的setter方法,如果没有抛出异常

    1.1、先通过下面方法获得相应的setter的名字SEL

    // 根据keyPath获取到setter name => setName
    // 把key的首字母大写,前面加上set,key就变成了setKey,然后再加:
    static NSString *setterFromKeyPath(NSString *kPath) {
        if (kPath.length <= 0) {
            return nil;
        }
        NSString *firstLetter = [[kPath substringToIndex:1] uppercaseString];
        NSString *remianLetters = [kPath substringFromIndex:1];
        return [NSString stringWithFormat:@"set%@%@:", firstLetter, remianLetters];
    }
    

    1.2 然后使用class_getInstanceMethod去获取setKey:方法的实现,如果没有实现,需要抛出异常:

    SEL setterSelector = NSSelectorFromString(setterFromKeyPath(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        NSString *reason = [NSString stringWithFormat:
                                @"Object %@ does not have a setter for key %@", self, keyPath];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                                           reason:reason
                                         userInfo:nil];
        return;
    }
    

    创建中间类,修改isa指向

    2.1 获取到当前类名

    Class currentClass = object_getClass(self);
    NSString *currentClassName = NSStringFromClass(currentClass);
    

    2.2 判断获取到的类名有没有我们定义的前缀。如果没有,我们就去创建新的中间类,继承于原来的类;并且修改被观察对象isa的指向,让其指向新创建的中间类。

    该静态常量是加给新创建类的类名前缀,用以标志中间类。

    static NSString * const kTKVOClassPrefix = @"TKVONotifying_";
    
    if (![currentClassName hasPrefix:kTKVOClassPrefix]) {
        // 生成中间类
        currentClass = [self makeKVOClassWithOriginalClassName:currentClassName];
        // 修改原对象isa的指向
        object_setClass(self, currentClass);
    }
    

    // 如果没有生成中间类,那就创建中间类

    - (Class)makeKVOClassWithOriginalClassName:(NSString *)originalClassName {
        // 生成kTKVOClassPrefix_class的类名
        NSString *tKvoClassName = [kTKVOClassPrefix stringByAppendingString:originalClassName];
        Class newClass = NSClassFromString(tKvoClassName);
        // 如果kvo class已经被注册过了 直接返回
        if (newClass) {
            return newClass;
        }
        
        Class originClass = object_getClass(self);
        Class tKvoClass = objc_allocateClassPair(originClass, tKvoClassName.UTF8String, 0);
        
        // 重写中间类的class方法,学习Apple的做法,对外隐藏中间类kvo_class
        Method tClassMethod = class_getInstanceMethod(originClass, @selector(class));
        const char *types = method_getTypeEncoding(tClassMethod);
        class_addMethod(tKvoClass, @selector(class), (IMP)kvo_class, types);
        objc_registerClassPair(tKvoClass);
        
        return tKvoClass;
    }
    
    static Class kvo_class(id self, SEL _cmd) {
        return class_getSuperclass(object_getClass(self));
    }
    

    执行到这里, 被观察对象的isa指针指向的已不是原类了, 而是新创建的中间类。

    重写setter方法

    中间类重写了好几个方法,其中最主要的就是setter。现在就需要判断中间类是否已经重写了setter,如果没有的话,就需要重写。

    // 是否已经存在了`setter`方法, 不存在就添加一个
    if (![self hasSelector:setterSelector]) {
        const char *type = method_getTypeEncoding(setterMethod);
        class_addMethod(currentClass, setterSelector, (IMP)kvo_setter, type);
    }
    
    // 判断当前类是否存在某个方法
    - (BOOL)hasSelector:(SEL)selector {
        Class currentClass = object_getClass(self);
        unsigned int methodCount = 0;
        Method *methodList = class_copyMethodList(currentClass, &methodCount);
        for (unsigned int i = 0; i < methodCount; i++) {
            SEL thisSelector = method_getName(methodList[i]);
            if (thisSelector == selector) {
                free(methodList);
                return YES;
            }
        }
        
        free(methodList);
        return NO;
    }
    

    重写的setter方法就是属性更改之后回调的关键。新方法在调用原方法给父类的属性赋值之后,通知每个观察者(调用传入的block)。

    static void kvo_setter(id self, SEL _cmd, id newValue) {
        NSString *setterName = NSStringFromSelector(_cmd);
        NSString *getterName = getterForSetter(setterName);
        
        // 如果不存在getter方法,就要抛出异常
        if (!getterName) {
            NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
            @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
            return;
        }
        
        id oldValue = [self valueForKey:getterName];
        // 调用原有类的setter方法
        // 实现`objc_super`的结构体
        struct objc_super tSuperclass = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))
        };
        
        // 这里需要做个类型强转, 否则会报too many argument的错误
        void (*objc_msgSendSuperCasted) (void *, SEL, id) = (void *)objc_msgSendSuper;
        objc_msgSendSuperCasted(&tSuperclass, _cmd, newValue);
        
        // 找出观察者的数组,调用对应对象的callback,给观察者回调
        NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kTKVOObservers));
        for (TObserverInfo *info in observers) {
            if ([info.keyPath isEqualToString:getterName]) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                    info.block(self, getterName, oldValue, newValue);
                });
            }
        }
    }
    
    // 通过setter得到getter
    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];
        
        // lower case the first letter
        NSString *firstLetter = [[getter substringToIndex:1] lowercaseString];
        getter = [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1)
                                           withString:firstLetter];
        return getter;
    }
    

    定义一个类,来存储观察者的相关信息

    @interface TObserverInfo: NSObject
    
    // 此时使用weak防止循环引用
    @property (nonatomic, weak) NSObject *observer;
    @property (nonatomic, copy) NSString *keyPath;
    @property (nonatomic, copy) TObserveingBlock block;
    
    @end
    
    @implementation TObserverInfo
    
    - (instancetype)initWithObserver:(NSObject *)ob keyPath:(NSString *)kp block:(TObserveingBlock)tob {
        self = [super init];
        if (self) {
            _observer = ob;
            _keyPath = kp;
            _block = tob;
        }
        return self;
    }
    
    @end
    

    存储观察者信息,用以回调、移除

    将观察的相关信息(观察者,被观察的key, 和回调的block)封装在 TObserverInfo类里,通过associatedObject存储起来。

    定义一个静态常量用来作为关联对象的key:

    static NSString * const kTKVOObservers = @"TKVOObservers";
    

    存储信息:

    TObserverInfo *info = [[TObserverInfo alloc] initWithObserver:observer keyPath:keyPath block:block];
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kTKVOObservers));
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *)(kTKVOObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject:info];
    

    到这里,我们自定义的KVO,注册以及回调部分已经完成。下面我们来看看移除观察者。

    移除观察者

    移除观察者只是需要我们将对应的观察者从动态关联的观察者列表中删除即可,所以我们只需要传入观察者和观察路径便于区分即可:

    - (void)removeTObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
        NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kTKVOObservers));
        if ([observers count] == 0) {
            return;
        }
        TObserverInfo *removeInfo;
        for (TObserverInfo *info in observers) {
            if (info.observer == observer && [info.keyPath isEqual:keyPath]) {
                removeInfo = info;
                break;
            }
        }
        [observers removeObject:removeInfo];
    }
    

    移除观察者优化

    KVO一般在观察者调用dealloc的时候进行移除观察者的操作,其实当我们观察的对象被销毁的时候,观察就没有意义了。所以我们也可以在观察对象dealloc的时候移除观察操作。

    仿照重写setter的时候,我们重写一下父类的dealloc方法。

    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    if (![self hasSelector:deallocSEL]) {
        Method deallocMethod = class_getInstanceMethod(class_getSuperclass(object_getClass(self)), deallocSEL);
        const char *deallocTypes = method_getTypeEncoding(deallocMethod);
        class_addMethod(currentClass, deallocSEL, (IMP)kvo_dealloc, deallocTypes);
    }
    
    static void kvo_dealloc(id self, SEL _cmd) {
        NSLog(@"===kvo_dealloc===");
        Class superClass = class_getSuperclass(object_getClass(self));
        object_setClass(self, superClass);
    }
    

    使用Method Swizzling交换方法

    NSObjec+KVO中重写load方法,在这个方法中,使用一个自定义的释放方法交换父类的dealloc方法。

    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            // 将父类的方法和自己重写的方法交换
            [self exchangeOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(kvoDealloc)];
        });
    }
    
    - (void)kvoDealloc {
        Class superClass = class_getSuperclass(object_getClass(self));
        object_setClass(self, superClass);
        [self kvoDealloc];
    }
    

    总结

    我们可以根据KVO的原理进行简单的自定义KVO,其思路如下:

      1. 确实自定义KVONSObject的分类
      1. 确定注册方法的参数、回调更改的方式为block,回调的参数
      1. 实现注册、回调方法
      • 检查被观察对象的类是否有相应的setter方法,如果没有就抛出异常
      • 检查被观察对象isa指向的类是不是一个KVO类,如果不是,新建一个继承于被观察对象的类的子类,并把isa指向这个新建的子类
      • 检查这个中间类是否重写过要观察属性的setter方法,如果没有,重写的setter方法,在setter方法中实现更改的回调
      • 添加观察者,使用关联对象存储观察的相关信息
      1. 实现移除观察者的方法,从关联对象的存储信息中心移除观察者

    相关文章

      网友评论

        本文标题:iOS-自定义KVO

        本文链接:https://www.haomeiwen.com/subject/wmvwchtx.html