iOS开发之KVC和KVO

KVC和KVO是Objective-C中经常被提到的两个术语。这篇文章将解释KVC和KVO的定义和最基本的用法,更多的信息请移步结尾部分的官方文档。


KVC(Key Value Coding)

定义

KVC的全称是Key Value Coding。这个名字会让人想到map的操作方式。实际上也正是这样,KVC可以让我们把NSObject对象当作一个map来用,即用字符串(Key)来访问NSObject对象的属性(Value)。

用法

我们只要使用Objective-C的property机制来定义成员变量,就可以使用KVC来访问属性了:

@interface BankAccount : NSObject
@property (nonatomic, assign) NSInteger balance;
@end

@interface Person : NSObject
- (instancetype)initWithName:(NSString*)name andBalance:(NSInteger)balance;

@property (nonatomic, strong) BankAccount *account;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray* friends;
@end

对于支持KVC的类,我们可以使用以下四个方法访问属性:

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

直接看例子:

- (void) testKVC {
  // 初始化
  Person *zhangSan = [[Person alloc] initWithName:@"ZhangSan" andBalance:20];
  zhangSan.friends = @[@"LiSi", @"WangWu"];

  // 读取属性
  NSLog(@"%@", [zhangSan valueForKey:@"name"]); // ZhangSan
  // 名字包含KeyPath的方法支持用点语法访问属性的属性。
  // 注意,数值型的属性会自动被封包为NSNumber。
  NSLog(@"%@", [zhangSan valueForKeyPath:@"account.balance"]);  // 20

  // 同样,支持点语法设置属性。
  [zhangSan setValue:@150 forKeyPath:@"account.balance"]; 
    
  // 支持NSArray和NSDictionary这类代表一对多含义的属性
  NSLog(@"%@", [zhangSan valueForKey:@"friends"]);  // 输出[LiSi, WangWu] 
}

用途

因为系统提供的类都支持KVC,所以我们可以用KVC的方式来设置SDK未暴露出来的属性,比如修改UITextField的占位符的颜色。(这种做法依赖了系统的内部的实现,应该尽量避免):

[self.textfield setValue:[UIColor greyColor] forKeyPath:@"_placeholderLabel.textColor"]; 

我们还可以让控件的标识和数据对象的属性完全一样,使用KVC就可以节省大量的判断逻辑。下面是官方文档里的例子:

// 不用KVC
- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row {
  ChildObject *child = [childrenArray objectAtIndex:row];
  if ([[column identifier] isEqualToString:@"name"]) {
    return [child name];
  }
  if ([[column identifier] isEqualToString:@"age"]) {
    return [child age];
  }
  if ([[column identifier] isEqualToString:@"favoriteColor"]) {
    return [child favoriteColor];
  }
  // And so on.
}

// 用KVC
- (id)tableView:(NSTableView *)tableview objectValueForTableColumn:(id)column row:(NSInteger)row {
  ChildObject *child = [childrenArray objectAtIndex:row];
  return [child valueForKey:[column identifier]];
}

更多的情况下,KVC是被用作别的技术的基础,比如下面的KVO。


KVO(Key Value Observing)

定义

KVO的全称是Key Value Observing。它是一种基于KVC的观察者模式的实现。如果不用KVO,我们要让一个类的属性实现观察者模式,得给这个类加上AddObserver/RemoveObserver方法,得在这个类里加一个数组保存所有的Observers,得在属性值变化的时候通知所有的Observer。虽然逻辑简单,但总归得折腾一会。
而有了KVO,我们可以使用很少的代码用上观察者模式了。

用法

@interface BankAccount : NSObject
@property (nonatomic, assign) NSInteger balance;
@end

@interface Person : NSObject
@property (nonatomic, strong) BankAccount *account;
@end

@implementation Person
 - (instancetype)init {
  ...   
  // 注册Observer:
  [self.account addObserver:self
                 forKeyPath:@"balance"
                    options:NSKeyValueObservingOptionNew |
                            NSKeyValueObservingOptionOld
                    context:nil];
  ...
}

- (void)dealloc {
  // 不要忘了removeObserver  
  [self.account removeObserver:self forKeyPath:@"balance"];
}

  // 属性变化的回调方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"balance"]) {
        NSLog(@"Balance was %@.", change[NSKeyValueChangeOldKey]);
        NSLog(@"Balance is %@ now.", change[NSKeyValueChangeNewKey]);
    }
}
@end

- (void)testKVO {
  Person *zhangSan = [[Person alloc] initWithName:@"ZhangSan" andBalance:20];
  // 无论是用点语法还是KVC的方法都会触发回调:
  zhangSan.account.balance = 150;
  [zhangSan setValue:@250 forKeyPath:@"account.balance"]; 
}

参考文档

Key-Value Coding Programming Guide
Key-Value Observing Programming Guide

推荐阅读更多精彩内容