说一下KVC和KVO

作者: 雷曼同学 | 来源:发表于2018-03-07 13:05 被阅读68次

    本篇采用简单的例子,来介绍 iOS 中的 KVC 和 KVO 的用法和实现原理。

    一、KVC

    1. KVC是什么

    KVC 即 Key-Value Coding,翻译成键值编码。它是一种不通过存取方法,而通过属性名称字符串间接访问属性的机制。

    2. KVC的用法

    KVC 常用到的方法有下面几个:

    - (id)valueForKey:(NSString *)key;
    - (void)setValue:(nullable id)value forKey:(NSString *)key;
    
    - (nullable id)valueForKeyPath:(NSString *)keyPath;
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
    

    前面的两个方法,以字符串的形式传入对象属性即可调用。私有属性也可以调用。如下代码所示:

    // 先声明一个对象ObjectA,同时具备私有属性和公有属性
    // ObjectA.h
    @interface ObjectA : NSObject
    
    @property (nonatomic, strong) NSString *publicPropertyString;
    
    @end
    
    
    // ObjectA.m
    @interface ObjectA ()
    
    @property (nonatomic, assign) NSInteger privatePropertyInteger;
    
    @end
    
    @implementation ObjectA
    
    - (instancetype)init {
        
        self = [super init];
        if (self) {
            self.publicPropertyString = @"publicPropertyString";
            self.privatePropertyInteger = 2000;
        }
        
        return self;
    }
    
    @end
    
    // 尝试调用
    ObjectA *objectA = [[ObjectA alloc] init];
    
    // 以下输出:publicPropertyString     
    NSLog(@"%@", [objectA valueForKey:@"publicPropertyString"]); 
    
    // 以下输出:2000       
    NSLog(@"%@", [objectA valueForKey:@"privatePropertyInteger"]);    
        
    // 将999赋值给privatePropertyInteger
    [objectA setValue:@(999) forKey:@"privatePropertyInteger"];
    
    // 以下输出:999
    NSLog(@"%@", [objectA valueForKey:@"privatePropertyInteger"]);
    
    

    后面两个方法支持传入用 . 连接的多层级属性,比如 school.schoolmaster.name同样支持私有属性。如下代码所示:

    // 再声明一个对象ObjectB,具备私有属性ObjectA
    // ObjectB.m
    @interface ObjectB ()
    
    @property (nonatomic, strong) ObjectA *objectA;
    
    @end
    
    @implementation ObjectB
    
    - (instancetype)init {
        
        self = [super init];
        if (self) {
            self.objectA = [[ObjectA alloc] init];
        }
        
        return self;
    }
    
    @end
    
    // 尝试调用
    ObjectB *objectB = [[ObjectB alloc] init];
    
    // 将999赋值给objectA的属性privatePropertyInteger
    [objectB setValue:@(999) forKeyPath:@"objectA.privatePropertyInteger"];
    
    // 以下输出:999
    NSLog(@"%@", [objectB valueForKeyPath:@"objectA.privatePropertyInteger"]);
    

    需要注意

    • value 的值为基本类型时,应该封装为 NSNumberNSValue
    • KVC不会自动调用键值验证方法。当字符串中的属性值不存在时,会直接抛出异常。
    • 可以先在类中重写 -validateValue: forKey: error: ,制定检查规则,然后手动调用该方法来验证。
    • KVC的一个重要应用是字典转模型

    3. KVC的原理

    为了设置或者获取对象属性,KVC按顺序使用如下技术:

    1. 获取对象属性时,检查是否存在 -<key>-is<key>(只针对布尔值有效)或者 -get<key> 的访问器方法,如果找到,就用这些方法来返回属性值;设置对象属性时,检查是否存在名为 -set<key>: 的方法,并使用它来设置属性值。对于 -get<key>-set<key>: 方法,将大写Key字符串的第一个字母,并与Cocoa的方法命名保持一致。
    2. 如果上述方法找不到,则检查名为 -_<key>-_is<key>(只针对布尔值有效)、 -_get<key>-_set<key>: 方法。
    3. 如果没有找到访问器方法,则尝试直接访问实例变量。实例变量可以是名为: <key>_<key>
    4. 如果仍未找到,则调用 valueForUndefinedKey:setValue:forUndefinedKey: 方法。这些方法的默认实现都是抛出异常,可以根据需要重写它们。

    可以看到,KVC会优先使用访问器方法来访问对象属性

    二、KVO

    1. KVO是什么

    KVO 即 Key-Value Observing,翻译成键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。

    2. KVO的用法

    KVO的使用主要分为三步:

    第一步,将目标对象添加为观察者。(注意这里用到了KVC,即通过字符串的方式去访问属性值。)

    - (void)addObserver:(NSObject *)observer
             forKeyPath:(NSString *)keyPath
                options:(NSKeyValueObservingOptions)options
                context:(nullable void *)context;
    

    第二步,实现接收通知的接口方法。

    - (void)observeValueForKeyPath:(nullable NSString *)keyPath
                          ofObject:(nullable id)object
                            change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                           context:(nullable void *)context;
    

    第三步,移除观察者。

    - (void)removeObserver:(NSObject *)observer
                forKeyPath:(NSString *)keyPath;
    

    在第一步中,NSKeyValueObservingOptions类型有四个取值,可以通过 | 来连接多个取值。分别为:

    • NSKeyValueObservingOptionNew,在属性值变化的时候回调,可以在change中取到变化后的值。
    • NSKeyValueObservingOptionOld,在属性值变化的时候回调,可以在change中取到变化前的值。
    • NSKeyValueObservingOptionInitial,在属性值初始化或者变化的时候回调,拿不到变化前后的值。
    • NSKeyValueObservingOptionPrior,在属性值变化前和变化后各回调一次,拿不到变化前后的值。

    举一个例子:

    @interface ObjectB ()
    
    @property (nonatomic, strong) ObjectA *objectA;
    
    @end
    
    @implementation ObjectB
    
    - (instancetype)init {
        
        self = [super init];
        if (self) {
            self.objectA = [[ObjectA alloc] init];
            // 第一步,将目标对象添加为观察者
            [_objectA addObserver:self
                       forKeyPath:@"privatePropertyInteger"
                          options:NSKeyValueObservingOptionNew
                          context:nil];
            
            [_objectA setValue:@(999) forKey:@"privatePropertyInteger"];
        }
        
        return self;
    }
    
    // 第二步,实现接收通知的接口方法
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                           context:(void *)context {
        
        // 这里最好判断一下object的类型和keyPath的值,不符合则交给父类处理
        if ([object isKindOfClass:[ObjectA class]] &&
            [keyPath isEqualToString:@"privatePropertyInteger"]) {
            
            NSLog(@"%@", change);  // 这里可以读取到 new = 999
            
        } else {
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                  context:context];
        }
    }
    
    // 第三步,移除观察者。
    - (void) dealloc {
        
        [_objectA removeObserver:self
                      forKeyPath:@"privatePropertyInteger"];
    }
    
    @end
    

    KVO可以在MVC模式中得到很好的应用。因为当Model发生变化时,通过KVO可以很方便地通知到Controller,从而通过Controller来改变View的展示。所以说KVO是解决Model和View同步的好办法。

    3. KVO的原理

    KVO的实现依赖于Runtime的强大动态能力。

    当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写这个类中任何被观察属性的 setter 方法。

    即当一个类型为 ObjectA 的对象,被添加了观察后,系统会生成一个 NSKVONotifying_ObjectA 类,并将对象的isa指针指向新的类,也就是说这个对象的类型发生了变化。这个类相比较于ObjectA,会重写以下几个方法。

    1. 重写setter

    在 setter 中,会添加以下两个方法的调用。

    - (void)willChangeValueForKey:(NSString *)key;
    - (void)didChangeValueForKey:(NSString *)key;
    

    然后在 didChangeValueForKey: 中,去调用:

    - (void)observeValueForKeyPath:(nullable NSString *)keyPath
                          ofObject:(nullable id)object
                            change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                           context:(nullable void *)context;
    

    于是实现了属性值修改的通知。因为 KVO 的原理是修改 setter 方法,因此使用 KVO 必须调用 setter 。若直接访问属性对象则没有效果。

    2. 重写class

    当修改了isa指向后,class的返回值不会变,但isa的值则发生改变。

    // 添加Observer之后
    
    // 输出ObjectA
    NSLog(@"%@", [_objectA class]);   
    
    // 输出NSKVONotifying_ObjectA(object_getClass方法返回isa指向)
    NSLog(@"%@", object_getClass(_objectA));    
    

    3. 重写dealloc

    系统重写 dealloc 方法来释放资源。

    4. 重写_isKVOA

    这个私有方法估计是用来标示该类是一个 KVO 机制声称的类。

    参考

    KVC和KVO的使用及原理
    KVC/KVO原理详解及编程指南
    iOS里的KVO模式

    获取更佳的阅读体验,请访问原文地址 【Lyman's Blog】说一下KVC和KVO

    相关文章

      网友评论

        本文标题:说一下KVC和KVO

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