美文网首页
KVO详解&使用及底层实现

KVO详解&使用及底层实现

作者: 猿人 | 来源:发表于2020-10-29 19:06 被阅读0次

    概念

    KVO (Key-Value Observing),看名字为键值观察,它是观察者设计模式的一种实现。简单来说 就是提供一种机制,对目标对象的某个属性添加观察,当该属性发生变化的时候,会通过KVO提供的接口自动通知观察者。【不需要对被观察对象添加任何额外代码,就可使用KVO机制】

    Key-Value Observing Programming Guide

    观察者模式(Observe Pattern)的定义:定义对象之间的一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

    使用

    注册观察者
    - (void)addObserver:(NSObject *)observer 
                     forKeyPath:(NSString *)keyPath 
                           options:(NSKeyValueObservingOptions)options
                           context:(nullable void *)context;
    
    • observer : 观察者
    • keyPath : 键路径参数,描述将要观察的属性
    • options :标识KVO希望变化如何传递给观察者,可以使用|进行多选(有四个选项)
    • context:上下文内存区,通常指为NULL,设置了上下文可以用上下文在 通知回调中做区分 相对来说安全

    1)、消息中的上下文指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。您可以指定NULL并完全依赖键路径字符串来确定更改通知的来源,但是这种方法可能会给对象的父类带来问题,该对象的超类也出于不同的原因而观察相同的键路径。
    2)、类中唯一命名的静态变量的地址提供了良好的上下文。在超类或子类中以类似方式选择的上下文不太可能重叠。您可以为整个类选择一个上下文,然后依靠通知消息中的关键路径字符串来确定更改的内容。另外,您可以为每个观察到的键路径创建一个不同的上下文,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析。清单1显示了以这种方式选择的balanceinterestRate属性的示例上下文。
    清单1:

    static void* PersonAccountBalanceContext =&PersonAccountBalanceContext;
    static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
    

    注意: 键值观察addObserver:forKeyPath:options:context:方法不维护对观察对象,被观察对象或上下文的强引用。您应该确保在必要时维护对观察对象和观察对象以及上下文的强烈引用。

    被观察者改变,收到通知,调用以下方法:
    -(void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary<NSString *,id> *)change
                          context:(void *)context {}
    
    • keyPath: 被观察者的属性
    • object : 被观察者对象
    • change:变化前后的值都存储在 change 字典中
    • context:注册观察者时,context 传过来的值

    当对象的观察属性的值更改时,观察者会收到一条observeValueForKeyPath:ofObject:change:context: 消息。所有观察者都必须实现此方法。
    1)、观察对象提供触发通知的键路径、作为相关对象的键路径、包含有关更改的详细信息的字典,以及在为这个键路径注册观察器时提供的上下文指针。
    2)、变更字典条目NSKeyValueChangeKindKey提供了关于发生的变更类型的信息。如果观察到的对象的值已经改变,NSKeyValueChangeKindKey条目返回nskeyvaluechangeset。根据观察者注册时指定的选项,更改字典中的NSKeyValueChangeOldKeyNSKeyValueChangeNewKey条目包含更改之前和之后的属性值。如果属性是对象,则直接提供该值。如果属性是标量C结构,则值包装在NSValue对象中(与键-值编码一样)。
    3)、如果观察到的属性是一个多对关系,那么NSKeyValueChangeKindKey条目也会指示关系中的对象是否被分别返回NSKeyValueChangeInsertion, NSKeyValueChangeRemoval,或NSKeyValueChangeReplacement所插入、删除或替换。
    NSKeyValueChangeIndexesKey的变化字典条目是一个NSIndexSet对象,指定变化关系中的索引。如果在观察者注册时NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld被指定为选项,那么在变化字典中的NSKeyValueChangeOldKeyNSKeyValueChangeNewKey条目是数组,其中包含了变化之前和之后相关对象的值。

    使用了上下文的判断区分

    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context {
     
        if (context == PersonAccountBalanceContext) {
            // Do something with the balance…
     
        } else if (context == PersonAccountInterestRateContext) {
            // Do something with the interest rate…
     
        } else {
         //任何无法识别的上下文都必须属于super
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                   context:context];
        }
    }
    
    移除观察者
    ///使用了上下文用这个移除
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
    ///没有使用用这个移除
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    

    在接收到removeObserver:forKeyPath:context:消息后,观察对象将不再接收任何observeValueForKeyPath:ofObject:change:context:指定键路径和对象的消息。
    当移除观察者时,请记住以下几点:
    1)、如果请求删除未注册的观察者,则会导致NSRangeException异常。你可以只调用removeObserver:forKeyPath:context:一次来对应addObserver:forKeyPath:options:context:,或者如果在你的应用中不可行,在一个try/catch块中放置removeObserver:forKeyPath:context:调用来处理潜在的异常。
    2)、释放后,观察者不会自动将其自身移除。被观察对象继续发送通知,而忽略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,您可以确保观察者在从内存中消失之前将自己删除。
    3)、该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间(例如,在init中或viewDidLoad中)注册为观察者,在释放过程中(通常在dealloc中)解除注册,以确保成对和有序地添加和删除消息,并确保观察者在从内存中释放之前被取消注册。

    触发自动kvo方法
    /// set方法
    [account setName:@"Savings"];
    ///kvo 赋值
    [account setValue:@"Savings" forKey:@"name"];
    [document setValue:@"Savings" forKeyPath:@"account.name"];
    /// 集合类型的需要  mutableArrayValueForKey 来获取集合 并增删改查 
    Transaction *newTransaction = <#Create a new transaction for the account#>;
    NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
     [transactions addObject:newTransaction]; 
    
    
    手动KVO

    KVO中,当被观察的属性改变时,观察者相应被触发。在某些情况下,您可能希望控制通知过程,例如,最大程度地减少出于应用程序特定原因而不必要的触发通知,或者将多个更改分组为一个通知。手动更改通知提供了执行此操作的方法

    • automaticallyNotifiesObserversForKey:(NSString *)theKey
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
     
        BOOL automatic = NO;
        if ([theKey isEqualToString:@"balance"]) {
            automatic = NO;
        }
        else {
            automatic = [super automaticallyNotifiesObserversForKey:theKey];
        }
        return automatic;
    }
    
    • 要实现手动通知需要 将 willChangeValueForKey:放在更改值之didChangeValueForKey:
    1. 普通操作单个属性:
    - (void)setBalance:(double)theBalance {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
    
    1. 如果单个操作更改多个属性,则必须嵌套更改通知,如下:
    - (void)setBalance:(double)theBalance {
        [self willChangeValueForKey:@"balance"];
        [self willChangeValueForKey:@"itemChanged"];
        _balance = theBalance;
        _itemChanged = _itemChanged+1;
        [self didChangeValueForKey:@"itemChanged"];
        [self didChangeValueForKey:@"balance"];
    }
    
    1. 对于有序的一对多关系,不仅必须指定已更改的键,而且还必须指定更改的类型和所涉及对象的索引。的类型变化的是NSKeyValueChange ,指定NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement。受影响对象的索引作为NSIndexSet对象传递 ;如下:
    - (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
        [self willChange:NSKeyValueChangeRemoval
            valuesAtIndexes:indexes forKey:@"transactions"];
     
        // Remove the transaction objects at the specified indexes.
     
        [self didChange:NSKeyValueChangeRemoval
            valuesAtIndexes:indexes forKey:@"transactions"];
    }
    

    底层原理探究

    官方文档

    Automatic key-value observing is implemented using a technique called isa-swizzling.

    The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

    When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
    You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

    • 自动键值观察是使用一种称为isa-swizzling的技术实现的。
    • isa指针,顾名思义,指向维护分派表的对象的类。这个分派表本质上包含了指向类实现的方法和其他数据的指针。
    • 当观察者为对象的属性注册时,被观察对象的isa指针将被修改,指向中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。
    • 您不应该依赖isa指针来确定类成员关系。相反,您应该使用类方法来确定对象实例的类。
    实践出真理
    • 当观察者为对象的属性注册时,被观察对象的isa指针将被修改
    截屏2020-10-29 下午5.36.52.png

    1、 通过 runtime api object_getClassName()方法 根据isa获取类
    2、 分别在注册前和注册后断点打印 发现的确 是isa被修改 被修改的isa的指向为 NSKVONotifying_A 类

    • 证明这个中间类到底和本类是什么关系

    通过如下封装方法 遍历类及子类

    - (void)printClasses:(Class)cls{
        
        // 注册类的总数
        int count = objc_getClassList(NULL, 0);
        // 创建一个数组, 其中包含给定对象
        NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
        // 获取所有已注册的类
        Class* classes = (Class*)malloc(sizeof(Class)*count);
        objc_getClassList(classes, count);
        for (int i = 0; i<count; i++) {
            if (cls == class_getSuperclass(classes[i])) {
                [mArray addObject:classes[i]];
            }
        }
        free(classes);
        NSLog(@"classes = %@", mArray);
    }
    
    截屏2020-10-29 下午5.52.39.png

    由此可以证明 中间类 为 本类的 子类 也就是继承关系

    • 中间类中有什么?

    可通过下面封装方法

    #pragma mark - 遍历方法-ivar-property
    - (void)printClassAllMethod:(Class)cls{
        unsigned int count = 0;
        Method *methodList = class_copyMethodList(cls, &count);
        for (int i = 0; i<count; i++) {
            Method method = methodList[i];
            SEL sel = method_getName(method);
            IMP imp = class_getMethodImplementation(cls, sel);
            NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
        }
        free(methodList);
    }
    
    截屏2020-10-29 下午6.03.52.png

    由此可以看出 KVO派生出的中间类 除了重写了要观察的属性的setter方法 还重写了class dealloc 因为 NSObject 都有这些方法。至于_isKVOA方法个人猜测是 派生类的一个 标识。

    • 那他是继承下来的方法 还是 新建立的 标识?
    截屏2020-10-29 下午6.30.32.png
    1. 在注册之前 向self.a 发送 _isKVOA的消息 发现返回 nil
    2. 在注册之前 向self.a 发送 不存在 的消息发现 直接报错 (由此可以看出 父类是有实现的 虽然没有见过)
    3. 注册之后 发现_isKVOA 不在返回nil了 (由此可以看出_isKVOA也是继承下来的方法)
    • dealloc中移除观察者后,isa指向是谁,以及中间类是否会销毁?
    截屏2020-10-29 下午6.49.49.png

    由此可以看出 在移除观察者之前 self.a 对象 的isa依旧指向的 中间类 在移除之后变回 A本类

    • 那么中间类从创建后,到dealloc方法中移除观察者之后,是否还存在?
    我们在上一级 来打印 A的子类情况 截屏2020-10-29 下午6.56.29.png

    通过子类的打印结果可以看出,中间类一旦生成,没有移除,没有销毁,还在内存中 -- 主要是考虑重用的想法,即中间类注册到内存中,为了考虑后续的重用问题,所以中间类一直存在

    总结

    基本原理
    • 当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法(类似手动KVO)。setter 方法随后负责通知观察对象属性的改变状况。
    底层实现
    • Apple 使用了 isa-swizzling技术 来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类, 该类继承自对象A的本类,此时A的实例对象isa的指向会指向 NSKVONotifying_A 新类。 且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。当移除 KVO观察者之后 才会将 A实例对象的isa指向 指回来 指为 A。
    • 中间类创建后就会存在内存中,不会被销毁
    • 除了重写 被观察的属性setter ,还有 class ,delloc ,_isKVOA.

    相关文章

      网友评论

          本文标题:KVO详解&使用及底层实现

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