说一下 KVC 和 KVO

956 阅读6分钟

本篇采用简单的例子,来介绍 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