• 【iOS】—— KVC与KVO


    一、KVC

    1.简单介绍

    KVC的全称是KeyValueCoding,俗称“键值编码”,可以通过一个key来访问某个属性;

    KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量;

    它是一个非正式的Protocol,提供一种机制来间接访问对象的属性,而不是通过调用Setter、Getter方法访问。KVO 就是基于 KVC 实现的关键技术之一。

    2.常见的API

    - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
    - (void)setValue:(id)value forKey:(NSString *)key;
    - (id)valueForKeyPath:(NSString *)keyPath;
    - (id)valueForKey:(NSString *)key;
    
    • 1
    • 2
    • 3
    • 4

    3.key和keyPath的区别

    key:只能接受当前类所具有的属性,不管是自己的,还是从父类继承过来的,如view.setValue(CGRectZero(),key: "frame");
    keypath:除了能接受当前类的属性,还能接受当前类属性的属性,即可以接受关系链,如view.setValue(5,keypath: "layer.cornerRadius");

    4.KVC原理

    4.1 setValue:forKey: 的原理(KVC赋值原理)

    • 首先会按照setKey_setKey的顺序查找方法,找到方法,直接调用方法并赋值;
    • 未找到方法,则调用+ (BOOL)accessInstanceVariablesDirectly(是否可以直接访问成员变量,默认返回YES);
    • accessInstanceVariablesDirectly方法返回YES,则按照_key_isKeykeyisKey的顺序查找成员变量,找到直接赋值,找不到则抛出NSUnknowKeyExpection异常;
    • accessInstanceVariablesDirectly方法返回NO,那么就会调用setValue:forUndefinedKey:并抛出NSUnknowKeyExpection异常;

    42324

    4.2 valueForKey:的原理(KVC取值原理)

    • 首先会按照getKeykeyisKey_key的顺序查找方法,找到直接调用取值
    • 若未找到,则查看+ (BOOL)accessInstanceVariablesDirectly的返回值,若返回NO,则直接抛出NSUnknowKeyExpection异常;
    • 若返回的YES,则按照_key_isKeykeyisKey的顺序查找成员变量,找到则取值;
    • 找不到则调用setValue:forUndefinedKey:抛出NSUnknowKeyExpection异常;

    4234234

    5.注意

    • key的值必须正确,如果拼写错误,会出现异常。
    • key的值是没有定义的,valueForUndefinedKey:这个方法会被调用,如果你自己写了这个方法,key的值出错就会调用到这里来。
    • 因为类可以反复嵌套,所以有个keyPath的概念,keyPath就是用.号来把一个一个key链接起来,这样就可以根据这个路径访问下去。
    • NSArrayNSSet等都支持KVC。
    • 可以通过KVC访问自定义类型的私有成员。
    • 如果对非对象传递一个nil值,KVC会调用setNIlValueForKey方法,我们可以重写这个方法来避免传递nil出现的错误,对象并不会调用这个方法,而是会直接报错。
    • 处理非对象,setValue时,如果要赋值的对象是基本类型,需要将值封装成NSNumber或者NSValue类型,valueForKey时,返回的是id类型的对象,基本数据类型也会被封装成NSNumber或者NSValuevalueForKey可以自动将值封装成对象,但是setValue:forKey:却不行。我们必须手动讲值类型转换成NSNumber/NSValue类型才能进行传递initWithBool:(BOOL)value

    6.KVC强大功能

    6.1 批量存值操作

    KVC还有更强大的功能,可以根据给定的一组key,获取到一组value,并且以字典的形式返回;

    - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    
    • 1

    6.2 批量赋值操作

    同样,也可以通过KVC进行批量操作,使用对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包好keyvalue的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给对象的属性赋值。

    - (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
    
    • 1

    实例代码:

    Person *personFirst = [[Person alloc] init];
        [personFirst setValue:@"buer" forKey:@"name"];
        [personFirst setValue:@11 forKey:@"age"];
        [personFirst setValue:@"男" forKey:@"sex"];
        NSDictionary *dictionaryFirst = [personFirst dictionaryWithValuesForKeys:@[@"name", @"age", @"sex"]];
        NSLog(@"dictionaryFirst = %@", dictionaryFirst);
        NSDictionary *dictionarySecond = @{@"name":@12, @"age":@11, @"sex":@"女"};
        Person *personSecond = [[Person alloc] init];
        [personSecond setValuesForKeysWithDictionary:dictionarySecond];
        NSLog(@"name = %@, age = %ld, sex = %@",personSecond.name, (long)personSecond.age, personSecond.sex);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4234234

    6.3 字典转模型

    如果model属性和dic不匹配,可以重写方法-(void)setValue:(id)value forUndefinedKey:(NSString *)key

    //StudentModel.h
    @interface StudentModel : NSObject
    
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, strong) NSString *age;
    @property (nonatomic, strong) NSString *studentSex;
    
    @end
    
    @implementation StudentModel
    
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key
    {
        if([key isEqualToString:@"sex"]) {
            self.studentSex = (NSString *)value;
        }
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    34234234

    6.4 模型转字典

    34234234

    二、KVO

    1.简单介绍

    KVO的全称是KeyValueObserving,俗称“键值监听",可以用于监听某个对象属性值的改变;

    KVO是苹果提供的在套事件通知机制。KVONSNotificationCenter都是iOS中观察者模式的一种实现,区别是:NSNotificationCenter可以是一对多的关系,而KVO是一对一的;

    2.KVO的使用

    2.1 KVO使用步骤

    注册KVO监听

    通过[addObserver:forKeyPath:options:context:]方法注册KVO,这样可以接收到keyPath属性的变化事件;

    • observer:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context: 方法。
    • keyPath:要观察的属性名称。要和属性声明的名称一致。
    • options:回调方法中收到被观察者的属性的旧值或新值等,对KVO机制进行配置,修改KVO通知的时机以及通知的内容
    • context:传入任意类型的对象,在"接收消息回调"的代码中可以接收到这个对象,是KVO中的一种传值方式。

    KVO监听实现

    通过方法[observeValueForKeyPath:ofObject:change:context:]实现KVO的监听;

    • keyPath:被观察对象的属性
    • object:被观察的对象
    • change:字典,存放相关的值,根据options传入的枚举来返回新值旧值
    • context:注册观察者的时候,context传递过来的值

    移除KVO监听

    在不需要监听的时候,通过方法[removeObserver:forKeyPath:],移除监听;

    2.2 KVO传值

    • 可以通过方法context传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO中的一种传值方式。
    • 通过KVO在ModelController之间进行通信。在这432描述

    2.3 其它

    如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的KVO调用,则可以重写下面方法。方法返回YES则表示可以调用相关对象的监听事件,如果返回NO则表示不可以调用相关对象的监听事件。

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"name"]) {
            return NO;
        } else {
            return [super automaticallyNotifiesObserversForKey:key];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样我们该类name属性就算变化了也不会调用监听事件了!注意:这个函数必须要定义在你不想调用的类中,这样才表示你不想监听该类的name属性。

    2.4 注意

    • 调用[removeObserver:forKeyPath:]需要在观察者消失之前,否则会导致Crash
    • 在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash
    • 观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash
    • KVO的addObserverremoveObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash
    • 在调用KVO时需要传入一个keyPath,由于keyPath是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash。我们可以利用系统的反射机制将keyPath反射出来,这样编译器可以在@selector()中进行合法性检查。324234234

    3.KVO原理

    3.1 KVO的本质是改变setter方法的调用:

    • 1、利用Runtime API动态生成一个子类NSKVONotifying_XXX,并且让instance对象的isa指向这个全新的子类,NSKVONotifying_XXXsuperclass指针指向原来的类;
    • 2、当修改instance对象的属性时,会调用Foundation_NSSetXXXValueAndNotify函数;
      2434234

    验证

    先定义一个person类:

    //person.h
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic, strong) NSString *name;
    @property (assign) NSInteger age;
    - (void)printInfo;
    @end
    
    //person.m
    #import "Person.h"
    #import <objc/runtime.h>
    
    @implementation Person
    
    - (void)printInfo {
        NSLog(@"isa:%@, super class:%@", NSStringFromClass(object_getClass(self)),
              class_getSuperclass(object_getClass(self)));
        
        NSLog(@"self:%@, [self superclass]:%@", self, [self superclass]);
        
        NSLog(@"age setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setAge:)));
        
        NSLog(@"name setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setName:)));
        NSLog(@"printInfo function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(printInfo)));
    }
    
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    添加监听和移除监听:

    self.myPerson = [[Person alloc] init];
        self.myPerson.name = @"p1";
        //打印监听前类信息
        [self.myPerson printInfo];
        // KVO是监听对象的属性值的改变的
        [self.myPerson addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
        self.myPerson.name = @"123";
        //打印监听后类信息
         [self.myPerson printInfo];
         [self.myPerson removeObserver:self forKeyPath:@"name"];
         //打印移除监听后类信息
         [self.myPerson printInfo];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    看其输出结果:
    4432234
    通过输出结果发现,其监听之后确实创建了一个中间类NSKVONotifying_Person,并且其监听属性的setter方法的地址还发生了改变,通过上边所说应该是重写了原来类的setter方法,从而达到了监听的目的。在我们移除监听之后,该类的isa又恢复指向为原来的类了,说明在监听的时候它就会创建一个中间类,监听结束之后就会撤销中间类,并且在我们监听的时候,该类的其他属性的setter地址也没有变化,我们大约能够推断出,他应该会通过superClass指针来获取之前的setter地址,为其赋值,因为superClass指向的还是原来的类。

    3.2 _NSSetXXXValueAndNotify的内部实现原理

    • 先调用willChangeValueForKey方法,
    • 再调用父类原来的setter方法
    • 最后调用didChangeValueForKey,其内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:);

    验证

    4234234
    我们在之前的willChangeValueForKey方法和didChangeValueForKey方法中间添加了输出信息,通过在控制台的输出,我们就能看到监听类的setter方法调用确实如上所说,并且监听事件还是在didChangeValueForKey方法中进行调用的,所以我们大胆推断didChangeValueForKey方法实现大概是这样的:432424

    综上总结,监听属性的setter方法会被中间类重写,并且重写后的结构大概是这样:53453453

    3.3 手动调用KVO

    我们大概知道了KVO的原理之后,那么我们想自己实现手动调用KVO岂不是手到擒来。步骤如下:

    • 1、修改类方法automaticallyNotifiesObserversForKey的返回值;
    • 2、调用KVO主要依靠两个方法,在属性发生改变之前调用willChangeValueForKey方法,在发生改变之后调用didChangeValueForKey方法即可;

    3.4 注意

    • 当观对象移除所有的监听后,会将观察对象的isa指向原来的类。
    • 当观察对象的监听全部移除后,动态生成的类不会注销,而是留在下次观察的时候在用,避免反复创建中间子类。

    4.KVO的所作所为

    • 在设置监听的时候,runtime动态的创建该类的子类,并重写了其setter方法。
    • class方法屏蔽内部实现隐藏了类的存在,避免开发者造成困扰。如果不重写class方法的话,会沿着继承链找到NSObject类中,而class的实现是返回了当前对象所属的类。
    • 重写了dealloc,进行一些释放收尾工作。
    • 添加_isKVO变量,判断是否被KVO动态生成子类。

    5.KVO关键

    isa-swizzling(类指针交换)就是把当前某个实例对象的isa指针指向一个新建造的中间类,在这个新建造的中间类上面做hook方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。
    4324234

    5.1 isa混写之后如何调用方法?

    • 调用监听的属性设置方法,例如setAge:,都会先调用NSKVONotify_Person对应的属性设置方法。
    • 调用非监听属性设置方法,如test,会通过NSKVONotify_Personsuperclass来找到Person类对象,再调用起[Person test]方法。

    5.2 为什么重写class方法?

    如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表、方法列表、父类缓存、方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_Person,就会将该类暴露出来。

    三、遇到的问题

    直接修改成员变量的值,会不会触发KVO?

    不会,KVO的本质是生成一个子类,重写父类的setter方法,在新的setter方法里面调用Foundation_NSSetXXXValueAndNotify函数,直接修改成员变量的值,不会执行setter,所以不会触发KVO

    KVC修改属性会触发KVO吗?

    会的 ,尽管setvalue:forkey:方法不一定会触发instance实例对象的setter:方法,但是setvalue:forkey:在更改成员变量值的时候,会手动调用willchangevalueforkeydidchangevalueforkey,会触发监听器的回调方法。

    KVO与代理的效率问题?

    KVO的效率比代理的效率低,因为KVO需要动态地生成这个类的子类NSKVONotifying_className,耗时。

    为什么KVC总提到对象的成员变量,而不是属性呢?

    如果是属性的话,系统自动帮我们实现了set方法,所以KVC总是可以找到它需要的setKey:方法。如果是成员变量,系统就不会为你实现set方法了。

    KVO怎么监听数组的元素变化?

    我们可以通过KVC来对数组进行添加元素的操作,这样就可以监听到了。通过KVCmutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArrayNSSet
    234232
    它的原理是,你调用的mutableArrayValueForKey会返回了一个新的数组,导致了原数组地址的改变,触发了KVO的监听。

  • 相关阅读:
    stm32——hal库学习笔记(SPI)
    postman忘记密码提交没响应
    【PCA降维】在人脸识别中的应用
    Dynamics 365应用程序开发- 8.利用Dynamics 365中的Azure扩展
    [机缘参悟-90]:《本质思考》- 本质思考的9个陷阱
    学习计划
    好书推荐《数据血缘分析原理与实践 》:数据治理神兵利器
    数据结构——排序
    SpringBoot 项目实战 ~ 9.数据缓存
    SpEl简单使用
  • 原文地址:https://blog.csdn.net/m0_55124878/article/details/126008853