KVO 的进一步理解

作者: CoderHG | 来源:发表于2018-05-13 14:59 被阅读23次

    这是一篇简单又丰富的简书,周末愉快!

    一、KVO概要及简单使用

    KVO 就是一种监听,那是如何做到监听的呢?首先创建一个简单的 Class,代码如下:

    #import <Foundation/Foundation.h>
    
    @interface KVOObject : NSObject
    
    // 姓名
    @property (nonatomic, copy) NSString* name;
    
    @end
    
    
    
    #import "KVOObject.h"
    
    @implementation KVOObject
    
    @end
    

    很简单, 就一个 Class,然后定义了一个 name 属性而已。

    一个简单的试验如下:

    // 创建一个 KVO 对象
    KVOObject* kvObj = [[KVOObject alloc] init];
    kvObj.name = @"HG";
    
    // 添加 KVO 监听
    [kvObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
    
    kvObj.name = @"CoderHG";
    
    // 移除 KVO 监听, 在 iOS 10之前不移除的话直接 crash, 之后的就没事了
    [kvObj removeObserver:self forKeyPath:@"name"];
    

    具体的监听方法代码如下:

    // KVO 的系统监听方法
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        NSLog(@"%@, %@, %@", keyPath, object, change);
    }
    

    以上就是一个简单而完整的 KVO 的使用场景,但是具体是什么原理呢?

    二、KVO 的原理初步认识

    都知道一个对象一旦添加了 KVO 监听,在本质上是系统动态的改变了该对象的 isa 指针。如果对 isa 不了解的话,可以看这个 OC 小专题
    想要知道 isa 有什么样的变动,先实现如下一个方法:

    // 打印具体的 cls 中的方法信息
    - (NSString*)printMethodNamesOfClass:(Class)cls {
        unsigned int count;
        // 获得方法数组
        Method *methodList = class_copyMethodList(cls, &count);
        
        // 存储方法名
        NSMutableString *methodNames = [NSMutableString string];
        
        // 遍历所有的方法
        for (int i = 0; i < count; i++) {
            // 获得方法
            Method method = methodList[i];
            // 获得方法名
            NSString *methodName = NSStringFromSelector(method_getName(method));
            // 拼接方法名
            [methodNames appendString:methodName];
            [methodNames appendString:@", "];
        }
        
        // 释放
        free(methodList);
        
        // 返回类名与方法列表
        return [NSString stringWithFormat:@"类名: %@ \n方法列表: %@", NSStringFromClass(cls), methodNames];
    }
    

    然后将以上的试验修改一下,如下:

    // 常规用法
    - (void)convention {
        // 创建一个 KVO 对象
        KVOObject* kvObj = [[KVOObject alloc] init];
        kvObj.name = @"HG";
        
        Class cls = object_getClass(kvObj);
        NSString* isaInfo = [self printMethodNamesOfClass:cls];
        
        NSLog(@"\n\n添加 KVO 之前:\n%@", isaInfo);
        
        
        // 添加 KVO 监听
        [kvObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
        
        kvObj.name = @"CoderHG";
        
        cls = object_getClass(kvObj);
        isaInfo = [self printMethodNamesOfClass:cls];
        NSLog(@"\n\n添加 KVO 之后:\n%@", isaInfo);
        
        // 移除 KVO 监听, 在 iOS 10之前不移除的话直接 crash, 之后的就没事了
        [kvObj removeObserver:self forKeyPath:@"name"];
    }
    

    日志有如下的打印:

    添加 KVO 之前:
    类名: KVOObject 
    方法列表: .cxx_destruct, name, setName:
    

    添加 KVO 之后:
    类名: NSKVONotifying_KVOObject 
    方法列表: setName:, class, dealloc, _isKVOA
    

    说明在添加 KVO 监听之后,isa 指针的值确实是变了,具体变化为:

    • 1、将之前的 KVOObject, 更换成 NSKVONotifying_KVOObject
    • 2、在 NSKVONotifying_KVOObject 中重写了 setName:、class 与 dealloc 方法,以及添加了一个 _isKVOA 方法。

    三、KVO 与 KVC 的那一份藕断丝连

    关于 KVC,强烈建议看一下这篇文章KVC 的原理概述,接下来将会在这篇文章的基础上做介绍。如果不看的话,可能你很难理解我所说的 非常规 KVC 调用是什么意思。虽然,我在这里仅仅是用到了那么一丁点的内容。
    首先创建一个 Class, 代码如下:

    #import <Foundation/Foundation.h>
    
    @interface KVO8KVCObject : NSObject
    
    @end
    
    
    #import "KVO8KVCObject.h"
    
    @interface KVO8KVCObject ()
    {
        // 非常规试验
        NSString* isGoddess;
    }
    
    @end
    
    @implementation KVO8KVCObject
    
    @end
    

    那接下来,我们想要表达一个什么问题呢?

    KVC 能否触发 KVO 监听?

    看了上面的 KVO8KVCObject 定义,我即将使用一个 KVC 的非常规调用来介绍,具体代码如下:

    // KVO 与 KVC 那一段藕断丝连的区域
    - (void)kvo8kvc {
       // 创建一个 KVO8KVC 对象
       KVO8KVCObject* kvO_CObj = [[KVO8KVCObject alloc] init];
       
       // 通过 KVC 赋值
       [kvO_CObj setValue:@"KJ" forKey:@"goddess"];
       
       Class cls = object_getClass(kvO_CObj);
       NSString* isaInfo = [self printMethodNamesOfClass:cls];
       
       NSLog(@"\n\n添加 KVO 之前:\n%@", isaInfo);
       
       
       // 添加 KVO 监听
       [kvO_CObj addObserver:self forKeyPath:@"goddess" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
       
       // 通过 KVC 赋值
       [kvO_CObj setValue:@"JK" forKey:@"goddess"];
       
       cls = object_getClass(kvO_CObj);
       isaInfo = [self printMethodNamesOfClass:cls];
       NSLog(@"\n\n添加 KVO 之后:\n%@", isaInfo);
       
       // 移除 KVO 监听, 在 iOS 10之前不移除的话直接 crash, 之后的就没事了
       [kvO_CObj removeObserver:self forKeyPath:@"goddess"];
    }
    

    看一下具体的 Log 打印,如下:

    添加 KVO 之前:
    类名: KVO8KVCObject 
    方法列表: .cxx_destruct
    

    goddess, <KVO8KVCObject: 0x60c00001b080>, {
        kind = 1;
        new = JK;
        old = KJ;
    }
    

    添加 KVO 之后:
    类名: NSKVONotifying_KVO8KVCObject 
    方法列表: class, dealloc, _isKVOA
    

    可以得出结论:
    KVC 能触发 KVO 监听。

    看到这里,也推翻了之前的一个结论:KVO 的正常触发的入口是 setter 方法,其实不是这样的,就如同上面的这个实验,在 NSKVONotifying_KVO8KVCObject 与 KVO8KVCObject 中根本就没有其对应的 setter 方法。

    本篇介绍,到这里就要告一段落了。在写本简书的时候,我有一个试验 Demo # OC2Nature,可以作为一个参考。具体请看 KVO 目录。
    同时别忘了看看本专题的其它文章 OC 小专题


    在之前也写过关于 KVO 的简书,虽然那时候理解还不是太深入,但是里面依旧是有新东西的。感兴趣的话可以去看看:

    相关文章

      网友评论

        本文标题:KVO 的进一步理解

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