认识KVO

作者: 凌云01 | 来源:发表于2020-08-10 23:47 被阅读0次

    问题

    KVO的本质是什么?(iOS用什么方式实现对一个对象的KVO?)
    如何手动触发KVO?
    直接修改成员变量会触发KVO么?

    • KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        Person *person1 = [[Person alloc] init];
        person1.age = 10;
        
        Person *person2 = [[Person alloc] init];
        person2.age = 20;
        
        // self 监听 person1的 age属性
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [person1 addObserver:self forKeyPath:@"age" options:options context:NULL];
        
        person1.age = 11;
        
        [person1 removeObserver:self forKeyPath:@"age"];
        
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        
        NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);
    
    }
    
    //控制台输出内容
    监听到<Person: 0x600000005550>的age改变了{
        kind = 1;
        new = 11;
        old = 10;
    }
    

    通过上面代码可以看到,在添加监听之后,age属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法。

    KVO的底层实现

    我们知道复制操作就是调用了set方法,我们可以重写Person类的age的set方法,观察是否是KVO在set方法内部做了一些操作来通知监听者。

    - (void)setAge:(int)age {
       _age = age;
    }
    

    发现即使重写了setAge方法,person1对象和person2对象调用同样的set方法,但是我们发现person1除了调用set方法之外还会另外执行监听器的observeValueForKeyPath方法。
    我们知道setAge方法是放在Person类对象里面的,而instance对象的isa指针指向的就是Person类对象,我们分别打印看一下person1和person2的isa的值:


    addObserver对person1对象的处理

    通过上图我们发现,person1对象执行过addObserver操作之后,person1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifying_Person类对象,这个类对象是OC Runtime运行时动态定义的一个新类,这个新类是Person的子类,其superClass指向Person类对象。而person2对象没有任何改变。

    首先我们知道,person2在调用setAge方法的时候,首先会通过person2对象中的isa指针找到Person类对象,然后在类对象中找到setAge方法。然后找到方法对应的实现。


    未使用KVO监听的对象

    那么person1对象在调用setAge方法的时候,肯定会根据person1的isa找到NSKVONotifying_Person,在NSKVONotifying_Person中找setAge的方法及实现。

    使用了KVO监听的对象

    经过查阅资料我们可以了解到
    NSKVONotifying_Person中的setAge方法中其实调用了 Fundation框架中C语言函数 _NSsetIntValueAndNotify,_NSsetIntValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey 将要改变方法,之后调用父类的setage方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。

    NSKVONotifyin_Person内部结构是怎样的?

    - (void)printMethodNamesOfClass:(Class)class{
        unsigned int count;
        //获取方法数组
        Method *methodList = class_copyMethodList(class, &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);
        //打印方法名
        NSLog(@"%@ %@", class, methodNames);
    }
    
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        Person *person1 = [[Person alloc] init];
        person1.age = 10;
        
        Person *person2 = [[Person alloc] init];
        person2.age = 20;
        
        // self 监听 person1的 age属性
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [person1 addObserver:self forKeyPath:@"age" options:options context:NULL];
        
        person1.age = 11;
        
        [self printMethodNamesOfClass:object_getClass(person1)];
        [self printMethodNamesOfClass:object_getClass(person2)];
    
        [person1 removeObserver:self forKeyPath:@"age"];
        
    }
    
    //打印内容分别为:
    NSKVONotifying_Person setAge:,class,dealloc,_isKVOA,
    Person age,setAge:
    

    通过上述代码我们发现NSKVONotifying_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA。
    这里NSKVONotifying_Person重写class方法是为了隐藏NSKVONotifying_Person。不被外界所看到。我们在person1添加过KVO监听之后,分别打印person1和person2对象的class可以发现他们都返回Person。

    NSLog(@"%@,%@",[person1 class],[person2 class]);
    // 打印结果 Person,Person
    

    如果NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到nsobject,而nsobect的class的实现大致为返回自己isa指向的类,返回person1的isa指向的类那么打印出来的类就是NSKVONotifyin_Person,但是apple不希望将NSKVONotifying_Person类暴露出来,并且不希望我们知道NSKVONotifying_Person内部实现,所以在内部重写了class类,直接返回Person类,所以外界在调用person1的class对象方法时,是Person类。这样person1给外界的感觉person1还是Person类,并不知道NSKVONotifying_Person子类的存在。

    那么我们可以猜测NSKVONotifying_Person内重写的class内部实现大致为

    - (Class) class {
         // 得到类对象,在找到类对象父类
         return class_getSuperclass(object_getClass(self));
    }
    

    最后我们来验证一下什么时候会调用observeValueForKeyPath:ofObject:change:context:方法
    //当监听对象的属性值发生改变时,就会调用

    - (void)setAge:(int)age
    {
        NSLog(@"setAge:");
        _age = age;
    }
    - (void)willChangeValueForKey:(NSString *)key
    {
        NSLog(@"willChangeValueForKey: - begin");
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey: - end");
    }
    - (void)didChangeValueForKey:(NSString *)key
    {
        NSLog(@"didChangeValueForKey: - begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey: - end");
    }
    
    //打印如下
    willChangeValueForKey: - begin
    willChangeValueForKey: - end
    setAge:
    didChangeValueForKey: - begin
    监听到<Person: 0x60000000cf90>的age改变了{
        kind = 1;
        new = 11;
        old = 10;
    }
    didChangeValueForKey: - end
    

    通过打印内容可以看到,确实在didChangeValueForKey方法内部已经调用了observer的observeValueForKeyPath:ofObject:change:context:方法。

    问题

    KVO的本质是什么?(iOS用什么方式实现对一个对象的KVO?)

    • 当一个对象使用KVO监听时,iOS系统会利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类

    • 子类拥有自己的set方法,当修改instance对象的属性时,内部会调用Foundation的_NSSetXXXValueAndNotify函数,函数内部实现为:

       willChangeValueForKey:
       父类原来的setter
       didChangeValueForKey:
       内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
      

    如何手动触发KVO?
    被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKey和didChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。

    //手动调用KVO
        [self.person1 willChangeValueForKey:@"age"];
        [self.person1 didChangeValueForKey:@"age"];
    

    直接修改成员变量会触发KVO么?
    不会,因为没有调用setter方法。因为KVO的本质是重写set方法,然后在set方法里依次调用willChangeValueForKey,原来的set方法,didChangeValueForKey,didChangeValueForKey内部会调用observer的observeValueForKeyPath: ofObject: change: context:方法


    文中如果有不对的地方欢迎指出。

    相关文章

      网友评论

          本文标题:认识KVO

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