KVO探索

作者: konglei | 来源:发表于2018-12-12 15:54 被阅读6次

    概述

    • KVO(Key-Value Observing) 是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于 KVO 的实现机制,所以对属性才会发生作用,一般继承自 NSObject 的对象都默认支持 KVO。

    • KVO 和 NSNotificationCenter 都是 iOS 中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO 是一对一的,而NSNotificationCenter是一对多的。KVO 对被监听对象无侵入性,不需要修改其内部代码即可实现监听。

    • KVO 的实现依赖于 OC 强大的Runtime

    • KVO 是 Cocoa 提供的一种基于 KVC 的机制

    实现原理

    1. KVO 是通过 isa-swizzling 技术实现的(这句话是整个 KVO 实现的重点)。

    2. 当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类(子类),在这个派生类中重写基类(父类)中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制。

    3. 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person
      ,每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察监听,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法

      派生类
    4. 键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey:didChangevlueForKey:在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就会记录旧的值。然后调用父类的setter方法更新key的值,而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

    5. KVO的这套实现机制中苹果还偷偷重写了-class()方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类

    使用

    • 注册观察者对象 :给对象Person的实例person(被观察者)通过 -addObserver:forKeyPath:options:context:添加观察者observer,指定要观察的属性keyPath,观察者就可以接受keyPath属性的变化事件。change含有kind、old、new三种
     typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
        NSKeyValueObservingOptionNew ,     //接受新值 默认
        NSKeyValueObservingOptionOld ,     //接受旧值
        NSKeyValueObservingOptionInitial , //在注册时立即接受一次回调,在改变时也会发送通知
        NSKeyValueObservingOptionPrior     //改变前发送一次,改变后发送一次
    };
    

    在调用addObserver后,KVO并不会对观察者进行强引用

    • 在观察者中实现 -observeValueForKeyPath:ofObject:change:context:方法,当keyPath变化时,KVO就会调用该方法通知观察者。object为被观察者对象。
    /* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
    kind指值得变化方式
    */
    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1,    //通过观察设置方法
        NSKeyValueChangeInsertion = 2,  //通过观察插入方法(容器)
        NSKeyValueChangeRemoval = 3,    //remove
        NSKeyValueChangeReplacement = 4,//替换
    };
    
    context 传入任意类型的对象,在接受消息会调的代码中姐可以接受到该对象
    
    • 当观察者不需要监听时,使用-removeObserver: forKeyPath:移除KVO。要在观察者消失前调用,-dealloc中。
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        /**
         根据 keyPath 确定KVO的出发模式是手动还是自动
         
         手动模式在key变化前后s需要手动调用 -willChangeValueForKey 和  -willChangeValueForKey
         [_person willChangeValueForKey:@"name"];
         _person.name = [NSString stringWithFormat:@"%d", a++];
         [_person willChangeValueForKey:@"name"];
         
         */
        if ([key isEqualToString:NSStringFromSelector(@selector(name))]) {
            return NO;
        }
        return YES;
    }
    
    /**
     属性依赖键 使对象获取其他对象的特定属性变化的通知机制
     */
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
        NSSet *keypaths = [super keyPathsForValuesAffectingValueForKey:key];
        
        /**
         当对当前类的dog属性添加观察者时,告诉系统dog的变化依赖于dog的那些属性的变化
         */
        if ([key isEqualToString:@"dog"]) {
            keypaths = [NSSet setWithObjects:@"_dog.color", @"_dog.age", nil];
        }
        return keypaths;
    }
    

    自定义

    添加观察者
    1. 通过Method判断是否有这个key对应的setter selector(因为值的变化是重写setter方法来达到目的,所以setter必须有),如果没有则Crash。
    2. 判断当前类是否是KVO子类,如果不是 则创建,并设置其isa指针。(实例的isa指针指向该实例的类,类的isa指针指向它的元类)
    3. 如果没有实现,则添加Key对应的setter方法。
    4. 将调用对象添加到数组中。
        // 1.
        SEL setSelector = NSSelectorFromString(setterForGetter(keyPath));
        Method setterMethod = class_getInstanceMethod(object_getClass(self), setSelector);
        if (!setterMethod) {
            NSString *reason =  [NSString stringWithFormat:@"Object %@ dose not have a setter for key :  %@",self, keyPath];
            @throw [NSException exceptionWithName:NSInvalidArgumentException
                                           reason:reason
                                         userInfo:nil];
            return;
        }
        
        // 2.
        Class class = object_getClass(self);
        NSString *classname = NSStringFromClass(class);
        if (![classname hasPrefix:KXSKVOClassPrefix]) {
            class = [self makeKvoClassWithOriginalClassName:classname];
            object_setClass(self, class);
        }
        
        // 3.
        if (![self hasSelector:setSelector]) {
            const char *types = method_getTypeEncoding(setterMethod);
            class_addMethod(class, setSelector, (IMP)kvo_setter, types);
        }
        
        // 4.
        KXSObservationInfo *observerInfo = [[KXSObservationInfo alloc] initWithObserver:observer key:keyPath changeBlock:changeBlock];
        
        NSMutableArray *observerArr = objc_getAssociatedObject(self, KXSKVOObserverProperty);
        if (!observerArr) {
            observerArr = [NSMutableArray array];
        }
        [observerArr addObject:observerInfo];
        objc_setAssociatedObject(self, KXSKVOObserverProperty, observerArr, OBJC_ASSOCIATION_COPY);
    
    创建子类,并设置其isa指针
    1. 判断是否存在KVO类,如果存在则返回。
    2. 如果不存在,则创建KVO类。
    3. 重写KVO类的class方法,指向自定义的IMP。
        // 1.
        NSString *kvoClazzName = [kISKVOClassPrefix stringByAppendingString:originalClazzName];
        Class clazz = NSClassFromString(kvoClazzName);
        
        if (clazz) {
            return clazz;
        }
        
        // 2.
        // class doesn't exist yet, make it
        Class originalClazz = object_getClass(self);
        
        /**
         创建一个类 为"class pair"分配空间
    
         @param superclass 继承的类
         @param name 类名
         objc_allocateClassPair(Class superclass, const char * name, 0)
         */
        Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
        
        // 3.
        // grab class method's signature so we can borrow it 获取类中的某个实例方法(减号方法)
        Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
        const char *types = method_getTypeEncoding(clazzMethod);
        // types 返回值类型 -> @"v@;@"
        // [p add]  [p setName:name]
        // objc_msgSend(p, @selector(add))   objc_msgSend(p, @selector(setName), name)
        class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
        // SEL selector 的简写,俗称方法选择器,实质存储的是方法的名称
        // IMP implement 的简写,俗称方法实现,看源码得知它就是一个函数指针
        // Method 对上述两者的一个包装结构.
        
        objc_registerClassPair(kvoClazz);
    
    重写子类的setter方法
    1. 获取旧值。
    2. 创建super的结构体,并向super发送属性的消息。这样相当于是调用原来类的setter方法,这一步是必须的。
    3. 遍历调用回调。
    // 1.
        NSString *setterName = NSStringFromSelector(_cmd);
        NSString *getterName = getterForSetter(setterName);
        
        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];
        
        // 2.
        struct objc_super superclazz = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))
        };
        // cast our pointer so the compiler won't complain
        void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
        
        // call super's setter, which is original class's setter method
        // objc_msgSend(objc_super->receiver, _cmd)
        // 从父类的方法列表开始找 _cmd 方法
        objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
        
        // 3.
        // look up observers and call the blocks
        NSMutableArray *observers = objc_getAssociatedObject(self,kISKVOAssociatedObservers);
        for (ISObservationInfo *each in observers) {
            if ([each.key isEqualToString:getterName]) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                    each.block(self, getterName, oldValue, newValue);
                });
            }
        }
    
    重写class方法调用
    static Class kvo_class(id self, SEL _cmd)
    {
        // 调用object_getClass函数后其返回的是一个Class类型,Class是objc_class定义的一个typedef别名,通过objc_class就可以获取到对象的isa指针指向的Class,也就是对象的类对象
        // 返回 类的父类 class_getSuperclass
        // 起到隐藏该子类,以“欺骗”外部调用者它就是起初的那个类
        return class_getSuperclass(object_getClass(self));
    }
    

    参考

    facebook 的 KVOController
    KVO底层原理及Block方式回调实现
    KVC/KVO原理详解及编程指南
    iOS开发-Runtime详解(简书)
    iOS开发-Runloop详解(简书)
    Type Encodings
    super与objc_msgSendSuper
    KVC/KVO原理详解及编程指南

    相关文章

      网友评论

          本文标题:KVO探索

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