• 【iOS开发】——KVO与KVC


    KVO

    KVO是什么?

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

    KVO可以监听单个属性的变化,也可以监听集合对象的变化。 通过 KVC mutableArrayValueForKey: 等方法获得代理对象,当代理对象的内部对象发生改变时,会回调 KVO 监听的方法。集合对象包含 NSArrayNSSet

    KVO的基本使用

    KVO的使用总共分为三个步骤:

    1. 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件
    2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者
    3. 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash

    注册观察者

     /*
    @observer:就是观察者,是谁想要观测对象的值的改变。
    @keyPath:就是想要观察的对象属性。
    @options:options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这样当属性值发生改变时我们可以同时获得旧值和新值,如果我们只填NSKeyValueObservingOptionNew则属性发生改变时只会获得新值。
    @context:想要携带的其他信息,比如一个字符串或者字典什么的。
    */
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • @observe:就是观察者,是谁想要观测对象的值的改变。
    • @keyPath:就是想要观察的对象属性。
    • @options:options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld这样当属性值发生改变时我们可以同时获得旧值和新值, 如果我们只填NSKeyValueObservingOptionNew则属性发生改变时只会获得新值。
    • @context:想要携带的其他信息,比如一个字符串或者字典什么的。

    监听回调

    /*
    @keyPath:观察的属性
    @object:观察的是哪个对象的属性
    @change:这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值
    @context:上面添加观察者时携带的信息
    */
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • @keyPath:观察的属性
    • @object:观察的是哪个对象的属性
    • change:这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值
    • @context:上面添加观察者时携带的信息

    移除监听
    当观察者不需要监听时,可以调用-(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath方法将KVO移除,我们需要在观察者消失之前进行处理,否则就crash

    • observer:观察者
    • keyPath: 被观察的对象的属性

    在这里插入图片描述
    但是我们真的可以不手动删除观察者吗?不会报错不等于是错的,可能会有隐患,不移除观察者,系统不会直接报错,但是存在隐患,如果观察者已经销毁了,被观察的对象没有销毁(比如我们对单例中的一个属性进行观察),然后又产生了KVO message,这时候就抛异常了,EXC_BAD_ACCESS

    调用方式

    自动调用

    调用KVO属性对象时,不仅可以通过点语法和set语法进行调用,还可以使用KVC方法

    //通过属性的点语法间接调用
    objc.name = @"";
    
    // 直接调用set方法
    [objc setName:@"Savings"];
    
    // 使用KVC的setValue:forKey:方法
    [objc setValue:@"Savings" forKey:@"name"];
    
    // 使用KVC的setValue:forKeyPath:方法
    [objc setValue:@"Savings" forKeyPath:@"account.name"];
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    手动调用

    KVO 在属性发生改变时的调用是自动的,如果想要手动控制这个调用时机,或想自己实现 KVO 属性的调用,则可以通过 KVO 提供的方法进行调用。

    手动调用的步骤:

    1. 第一步我们需要认识下面这个方法,如果想要手动调用或自己实现KVO需要重写该方法该方法返回YES表示可以调用,返回NO则表示不可以调用。
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
        BOOL automatic = NO;
        if ([theKey isEqualToString:@"name"]) {
            automatic = NO;//对该key禁用系统自动通知,若要直接禁用该类的KVO则直接返回NO;
        }
        else {
            automatic = [super automaticallyNotifiesObserversForKey:theKey];
        }
        return automatic;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 第二步我们需要重写setter方法
    - (void)setName:(NSString *)name {
        if (name != _name) {
            [self willChangeValueForKey:@"name"];
            _name = name;
            [self didChangeValueForKey:@"name"];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    KVO实现原理

    KVO是通过isa 混写(isa-swizzling)技术实现的。 在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。 所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

    看一下这段代码:

    NSKeyValueObservingOptions option = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
    
    NSLog(@"person1添加KVO监听对象之前-类对象 -%@", object_getClass(self.person1));
    NSLog(@"person1添加KVO监听之前-方法实现 -%p", [self.person1 methodForSelector:@selector(setAge:)]);
    NSLog(@"person1添加KVO监听之前-元类对象 -%@", object_getClass(object_getClass(self.person1)));
    
    [self.person1 addObserver:self forKeyPath:@"age" options:option context:@"age chage"];
    
    NSLog(@"person1添加KVO监听对象之后-类对象 -%@", object_getClass(self.person1));
    NSLog(@"person1添加KVO监听之后-方法实现 -%p", [self.person1 methodForSelector:@selector(setAge:)]);
    NSLog(@"person1添加KVO监听之后-元类对象 -%@", object_getClass(object_getClass(self.person1)));
    
    //打印结果
    KVO-test[1214:513029] person1添加KVO监听对象之前-类对象 -Person
    KVO-test[1214:513029] person1添加KVO监听之前-方法实现 -0x100411470
    KVO-test[1214:513029] person1添加KVO监听之前-元类对象 -Person
    
    KVO-test[1214:513029] person1添加KVO监听对象之后-类对象 -NSKVONotifying_Person
    KVO-test[1214:513029] person1添加KVO监听之后-方法实现 -0x10076c844
    KVO-test[1214:513029] person1添加KVO监听之后-元类对象 -NSKVONotifying_Person
    
    //通过地址查找方法
    (lldb) p (IMP)0x10f24b470
    (IMP) $0 = 0x000000010f24b470 (KVO-test`-[Person setAge:] at Person.h:15)
    (lldb) p (IMP)0x10f5a6844
    (IMP) $1 = 0x000000010f5a6844 (Foundation`_NSSetLongLongValueAndNotify)
    
    
    • 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

    通过上面的代码,我们可以发现KVO添加以后发生了如下变化:

    • person指向的类对象和元类对象,以及 setAge: 均发生了变化;
    • 添加KVO后,person 中的 isa 指向了 NSKVONotifying_Person 类对象;
    • 添加 KVO 之后,setAge: 的实现调用的是:Foundation 中 _NSSetLongLongValueAndNotify 方法;

    KVO会在运行时动态创建一个新类将对象的isa指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xxx的格式。

    这也就是上边代码person 中的 isa 从开始指向Person类对象,变成指向了 NSKVONotifying_Person 类对象

    未使用KVO监听对象是,对象和类对象之间的关系如下:
    在这里插入图片描述
    使用KVO监听对象后,对象和类对象之间会添加一个中间对象:
    在这里插入图片描述

    NSKVONotifying_Person类内部实现

    我们来看一下这个中间类NSKVONotifying_Person的内部是如何实现的

    - (void)setAge:(int)age{
        _NSSet*ValueAndNotify();//这个方法调用顺序是什么,它是在调用何处方法,都在setter方法改变中详解
    }
    
    - (Class)class {
        return [LDPerson class];
    }
    
    - (void)dealloc {
        // 收尾工作
    }
    
    - (BOOL)_isKVOA {
        return YES;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • isa混写之后如何调用方法
    1. 调用监听的属性设置方法,如 setAge:,都会先调用 NSKVONotify_Person 对应的属性设置方法;
    2. 调用非监听属性设置方法,如 test,会通过 NSKVONotify_Person 的 superclass,找到 Person 类对象,再调用其 [Person test] 方法
    • 为什么重写class方法
    • 如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_Person,就会将该类暴露出来,也给开发者造成困扰,写的是Person,添加KVO之后class方法返回怎么是另一个类。
      
      • 1
    • _isKVOA有什么作用
    • 这个方法可以当做使用了KVO的一个标记,系统可能也是这么用的。如果我们想判断当前类是否是KVO动态生成的类,就可以从方法列表中搜索这个方法。
      
      • 1

    setter实现不同

    我们可以看到在添加KVO后set方法的实现从调用setAge:方法变成调用_NSSetIntValueAndNotify这样一个C函数

    我们不知道_NSSetIntValueAndNotify到底是什么样的函数,无法得知它的真实结构,也无法去重写NSKVONotifying_Person这个类,但我们可以利用它的父类Person类来分析其执行过程。

    - (void)setAge:(int)age{
        _age = age;
        NSLog(@"setAge:");
    }
    
    - (void)willChangeValueForKey:(NSString *)key{
        [super willChangeValueForKey:key];
        NSLog(@"willChangeValueForKey");
    }
    
    - (void)didChangeValueForKey:(NSString *)key{
        NSLog(@"didChangeValueForKey - begin");
        [super didChangeValueForKey:key];
        NSLog(@"didChangeValueForKey - end");
    }
    @end
    
    //打印结果
    KVO-test[1457:637227] willChangeValueForKey
    KVO-test[1457:637227] setAge:
    KVO-test[1457:637227] didChangeValueForKey - begin
    KVO-test[1457:637227] didChangeValueForKey - end
    KVO-test[1457:637227] willChangeValueForKey
    KVO-test[1457:637227] didChangeValueForKey - begin
    KVO-test[1457:637227] didChangeValueForKey - 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

    通过打印结果,我们可以得出以下结论:

    1. 首先调用willChangeValueForKey:方法。
    2. 然后调用setAge:方法真正的改变属性的值。
    3. 开始调用didChangeValueForKey:这个方法,调用[super didChangeValueForKey:key]时会通知监听者属性值已经改变,然后监听者执行自己的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context这个方法。

    总结KVO

    看一下KVO的整个执行流程图:
    在这里插入图片描述

    KVC

    KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,很多高级的iOS开发技巧都是基于KVC实现的。 KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过字符串来访问对应的属性方法或成员变量。NSKeyValueCoding中提供了KVC通用的访问方法,分别是getter方法valueForKeysetter方法setValue:forKey,以及其衍生的keyPath方法这两个方法是各个类通用的。并且由KVC提供默认的实现,我们也可以自己重写对应的方法来改变实现。

    KVC基础操作

    KVC取值

    • 通过key
    - (nullable id)valueForKey:(NSString *)key;//直接通过Key来取值
    
    
    • 1
    • 2
    • 通过keyPath
    - (nullable id)valueForKeyPath:(NSString *)keyPath;//通过KeyPath来取值
    
    • 1

    基于getter取值底层实现

    当调用valueForKey的代码时,其搜索方式如下:
    在这里插入图片描述

    1. 通过getter方法搜索实例,按照get, , is, _的顺序查找getter`方法。如果发现符合的方法,就调用对应的方法并拿着结果跳转到第五步。否则,就继续到下一步。
    2. 如果没有找到简单的getter方法,则搜索其匹配模式的方法countOfobjectInAtIndex:AtIndexes:。如果找到其中的第一个和其他两个中的一个,则就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类)。或者说给这个代理集合发送属于NSArray的方法,就会以countOf,objectInAtIndexAtIndexes这几个方法组合的形式调用。否则,继续到第三步。代理对象随后将NSArray接收到的countOf objectInAtIndex:AtIndexes:消息给符合KVC规则的调用方。当代理对象和KVC调用方通过上面方法一起工作时,就会允许其行为类似于NSArray一样。
    3. 如果没有找到NSArray简单存取方法,或者NSArray存取方法组。那么会同时查找countOfenumeratorOfmemberOf:名的方法。如果找到三个方法,则创建一个集合代理对象,该对象响应所有NSSet方法并返回。 否则,继续执行第四步。给这个代理对象发NSSet的消息,就会以countOfenumeratorOf,memberOf组合的形式调用。
    4. 如果没有发现简单getter方法,或集合存取方法组,以及接收类方法accessInstanceVariablesDirectly是返回YES的。 搜索一个名为__isis的实例,根据他们的顺序。如果发现对应的实例,则立刻获得实例可用的值并跳转到第五步如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:。
    5. 如果取回的是一个对象指针,则直接返回这个结果。 如果取回的是一个基础数据类型,但是这个基础数据类型是被NSNumber支持的,则存储为NSNumber并返回。如果取回的是一个不支持NSNumber的基础数据类型,则通过NSValue进行存储并返回。
    6. 如果所有情况都失败,则调用valueForUndefinedKey:方法并抛出异常,这是默认行为。但是子类可以重写此方法。

    KVC设值

    • 通过key
      直接将属性名当做key,并设置value,即可对属性进行赋值。 只能访问当前类所具有的属性
    - (void)setValue:(nullable id)value forKey:(NSString *)key;//通过Key来设值
    
    • 1
    • 通过keyPath
      除了能访问当前类的属性,还能访问当前类属性的属性,多层访问
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;//通过KeyPath来设值
    
    • 1

    放一个关于多层访问的demo:

    //Person类的定义
    #import <Foundation/Foundation.h>
    #import "Room.h"
    #import "Son.h"
    NS_ASSUME_NONNULL_BEGIN
    @class Son;
    @interface Person : NSObject
    @property (nonatomic,strong)Son *son;
    @end
    
    NS_ASSUME_NONNULL_END
    
    //Son类的定义
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Son : NSObject
    @property (nonatomic,copy) NSString * name;
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    //main函数
    #import <Foundation/Foundation.h>
    #import "Person.h"
    #import "Room.h"
    #import "Son.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            Person *person1 = [[Person alloc] init];
            person1.son = [[Son alloc] init];
            [person1 setValue:@"Yep" forKeyPath:@"son.name"];
            NSLog(@"%@",person1.son.name);
        }
        return 0;
    }
    
    • 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
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    从这里也就能看出来key和keyPath的区别就是前者是只能访问本类的属性,而后者可以访问当前类属性的属性

    基于setter赋值底层实现

    这是setValue:forKey:的默认实现,给定输入参数valuekey。试图在接收调用对象的内部,设置属性名为keyvalue,通过下面的步骤:
    在这里插入图片描述

    1. 查找set:_set命名的setter按照这个顺序,如果找到的话,代码通过setter方法完成设置。
    2. 如果没有找到setter方法KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly的返回值,如果accessInstanceVariablesDirectly类属性返回YES,则查找一个命名规则为__isis的实例变量。根据这个顺序,如果发现则将value赋值给实例变量,如果返回值为NO,KVC会执行setValue:forUndefinedKey:方法。
    3. 果没有发现setter或实例变量,则调用setValue:forUndefinedKey:方法,并默认提出一个异常,但是一个NSObject的子类可以提出合适的行为。

    多值操作

    KVC可以根据给定的一组key,获取到一组value,并且以字典的形式返回,获取到字典后可以通过key从字典中获取到value

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

    上面是利用字典整体取值,接下来我们来看一下如何批量赋值:在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含keyvalue的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。

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

    看一个小demo:

    //创建一个Student模型,里面的字符串名称必须和key的名称对应,不然该方法会崩溃
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Student : NSObject
    @property(nonatomic, strong) NSString *name;
    @property(nonatomic, strong) NSString *sex;
    @property(nonatomic, strong) NSString *age;
    @property(nonatomic, strong) NSString *Aka;
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    //在main函数里,声明Stduent类并利用批量赋值给Student对应的属性
    Student *student = [[Student alloc] init];
    NSDictionary *dictionary = @{@"name":@"wyf",@"sex":@"boy",@"Aka":@"Yep"};
    //批量赋值
    [student setValuesForKeysWithDictionary:dictionary];
    NSLog(@"%@",student);
    NSLog(@"%@,%@,%@,%@",student.name,student.sex,student.age,student.Aka);
    NSDictionary *dictionaryStudent = [student dictionaryWithValuesForKeys:@[@"name",@"sex",@"Aka"]];
    NSLog(@"dictionaryStudent : %@",dictionaryStudent);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    通过打印结果我们可以看到:
    在这里插入图片描述
    我们可以看到打印结果中Student里有一个属性的值为null,这是为什么呢?因为在 Student属性和 dictionary 不匹配,可以重写方法 -(void)setValue:(id)value forUndefinedKey:(NSString *)key

    - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
        if([key isEqualToString:@"age"]) {
            self.age = (NSString *)value;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    总结KVC

    通过KVC修改属性会触发KVO么?

    会触发,我们看一下以下的代码:

       Person *p1 = [[Person alloc]init];
       p1.age  = 10;
       // --------------- VS ----------------
       Person *p2 = [[Person alloc]init];
       [p2 setValue:@10 forKey:@"age"];
    
    • 1
    • 2
    • 3
    • 4
    • 5

    它们的本质都一样,都会调用[self willChangeValueForKey:key]; [self didChangeValueForKey:key];

    KVC的赋值和取值过程是怎样的?原理是什么?

    setValue:forKey: 赋值的原理
    ① 首先会查找setKey:_setKey: (按顺序查找);
    ② 如果有直接调用,如果没有,先查看accessInstanceVariablesDirectly方法
    ③ 如果可以,访问会按照 _key_isKeykeyiskey顺序查找成员变量,找到直接赋值;
    ④ 未找到报错NSUnkonwKeyException错误。
    在这里插入图片描述

    valueForKey: 取值的原理
    kvc取值按照 getKeykeyiskey_key 顺序查找;
    存在直接调用,如果没找到,同样会先查看accessInstanceVariablesDirectly方法
    如果可以访问会按照 _key_isKeykeyiskey的顺序查找成员变量,找到直接赋值
    ④ 未找到报错NSUnkonwKeyException错误。
    在这里插入图片描述

    用KVC来访问和修改私有变量

    KVC的本质是操作方法列表以及在内存中查找实例变量。
    我们可以利用这个特性访问类的私有变量。

    同样如果不想让外界使用KVC的方法访问类的成员变量,可以将accessInstanceVariablesDirectly属性设置为NO

  • 相关阅读:
    一键实现冒泡排序算法,代码质量有保障!
    古代汉语(王力版)笔记
    企业级软件开发流程
    【代码精读】optee的异常向量表
    深入理解CSS中的块格式化上下文(BFC)
    【晶振专题】晶振学习笔记——ST AN2867应用手册 1
    Ajax 相关问题
    UE4 通过按键升降电梯
    element ui框架(登陆状态保存)
    科技云报道:大模型会给操作系统带来什么样的想象?
  • 原文地址:https://blog.csdn.net/weixin_51638861/article/details/126108050