KVC 和 KVO

     注:对该文章 本人做了适当的修改,如下。要看原本请戳进链接。         转自      objc中国  点击打开链接      

Key-value coding (KVC) 和 key-value observing (KVO) 是两种能让我们驾驭 Objective-C 动态特性并简化代码的机制。在这篇文章里,我们将接触一些如何利用这些特性的例子。

KVO中 model 对象

在 Cocoa 的模型-视图-控制器 (Model-view-controller)架构里,控制器负责让视图和模型同步。这一共有两步:当 model 对象改变的时候,视图应该随之改变以反映模型的变化;当用户和控制器交互的时候,模型也应该做出相应的改变。

KVO 能帮助我们让视图和模型保持同步。控制器可以观察视图依赖的属性变化。

让我们看一个例子:我们的模型类 LabColor 代表一种 Lab色彩空间里的颜色。和 RGB 不同,这种色彩空间有三个元素 Lab。我们要做一个用来改变这些值的滑块和一个显示颜色的方块区域

我们的模型类有以下三个用来代表颜色的属性:

@property (nonatomic) double lComponent;
@property (nonatomic) double aComponent;
@property (nonatomic) double bComponent;

依赖的属性

我们需要从这个类创建一个  UIColor  对象来显示出颜色。我们添加三个额外的属性,分别对应 R, G, B:

@property (nonatomic, readonly) double redComponent;
@property (nonatomic, readonly) double greenComponent;
@property (nonatomic, readonly) double blueComponent;

@property (nonatomic, strong, readonly) UIColor *color;

有了这些以后,我们就可以创建这个类(LabColor)的接口了:

@interface LabColor : NSObject

@property (nonatomic) double lComponent;
@property (nonatomic) double aComponent;
@property (nonatomic) double bComponent;

@property (nonatomic, readonly) double redComponent;
@property (nonatomic, readonly) double greenComponent;
@property (nonatomic, readonly) double blueComponent;

@property (nonatomic, strong, readonly) UIColor *color;

@end

维基百科 提供了转换 RGB 到 Lab 色彩空间的算法。写成方法之后如下所示: (以下方法请直接无视)

- (double)greenComponent;
{
    return D65TristimulusValues[1] * inverseF(1./116. * (self.lComponent + 16) + 1./500. * self.aComponent);
}

[...]

- (UIColor *)color
{
    return [UIColor colorWithRed:self.redComponent * 0.01 green:self.greenComponent * 0.01 blue:self.blueComponent * 0.01 alpha:1.];
}

这些代码没什么令人激动的地方。有趣的是 greenComponent 属性依赖于 lComponent  aComponent不论何时设置 lComponent的值我们需要让 RGB 三个 component 中与其相关的成员以及 color 属性都要得到通知以保持一致。这一点这在 KVO 中很重要。

Foundation 框架提供的表示属性依赖的机制如下:

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key

更详细的如下:

+ (NSSet *)keyPathsForValuesAffecting<键名>
在我们的例子中如下:( 假如你设置了属性,当你打入keyPaths时 系统将自动弹出方法 )

+ (NSSet *)keyPathsForValuesAffectingRedComponent
{
    return [NSSet setWithObject:@"lComponent"];
}

+ (NSSet *)keyPathsForValuesAffectingGreenComponent
{
    return [NSSet setWithObjects:@"lComponent", @"aComponent", nil];
}

+ (NSSet *)keyPathsForValuesAffectingBlueComponent
{
    return [NSSet setWithObjects:@"lComponent", @"bComponent", nil];
}

+ (NSSet *)keyPathsForValuesAffectingColor
{
    return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil];
}

现在我们完整的表达了属性之间的依赖关系。请注意,我们可以把这些属性链接起来( 当改变某个值 会自动调用另外的方法 )。打个比方,如果我们写一个子类去 override redComponent  方法,这些依赖关系仍然能正常工作。

观察变化

现在让我们目光转向控制器。 NSViewController 的子类拥有 LabColor model 对象作为其属性。

@interface ViewController ()

@property (nonatomic, strong) LabColor *labColor;

@end
我们把视图控制器注册为观察者来接收 KVO 的通知 ,这可以用以下  NSObject  的方法来实现:

- (void)addObserver:(NSObject *)anObserver
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context

这会让以下方法:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
在当 keyPath 的值改变的时候在观察者 anObserver  上面被调用。这个 API 看起来有一点吓人。更糟糕的是,我们还得记得调用以下的方法

- (void)removeObserver:(NSObject *)anObserver
            forKeyPath:(NSString *)keyPath

移除观察者,否则我们我们的 app 会因为某些奇怪的原因崩溃。

对于大多数的应用来说,KVO 可以通过辅助类用一种更简单优雅的方式实现。我们在视图控制器添加以下的观察记号(Observation token)属性:

@property (nonatomic, strong) id colorObserveToken;

当 labColor 在视图控制器中被设置时,我们只要 override labColor  的 setter 方法就行了

- (void)setLabColor:(LabColor *)labColor
{
    _labColor = labColor;
    self.colorObserveToken = [KeyValueObserver observeObject:labColor
                                                     keyPath:@"color"
                                                      target:self
                                                    selector:@selector(colorDidChange:)
                                                     options:NSKeyValueObservingOptionInitial];
}

- (void)colorDidChange:(NSDictionary *)change;
{
    self.colorView.backgroundColor = self.labColor.color;
}

KeyValueObserver 辅助类  封装了  -addObserver:forKeyPath:options:context: -observeValueForKeyPath:ofObject:change:context: -removeObserverForKeyPath:  的调用,让视图控制器远离杂乱的代码

整合到一起

视图控制器需要对 Lab 的滑块控制做出反应:

- (IBAction)updateLComponent:(UISlider *)sender;
{
    self.labColor.lComponent = sender.value;
}

- (IBAction)updateAComponent:(UISlider *)sender;
{
    self.labColor.aComponent = sender.value;
}

- (IBAction)updateBComponent:(UISlider *)sender;
{
    self.labColor.bComponent = sender.value;
}
所有的代码都在我们的 GitHub  示例代码  中找到。

手动通知 vs 自动通知

我们刚才所做的事情有点神奇,但是实际上发生的事情是,当 LabColor 实例的 -setLComponent: 等方法被调用的时候以下方法

- (void)willChangeValueForKey:(NSString *)key	

 - (void)didChangeValueForKey:(NSString *)key

会在运行 -setLComponent: 中的代码之前以及之后被自动调用。如果我们写了 -setLComponent: 或者我们选择使用自动 synthesize 的 lComponent 的 accessor 到时候就会发生这样的事情。

有些情况下当我们需要 override -setLComponent: 并且我们要控制是否发送键值改变的通知的时候,我们要做以下的事情:

+ (BOOL)automaticallyNotifiesObserversForLComponent;
{
    return NO;
}

- (void)setLComponent:(double)lComponent;
{
    if (_lComponent == lComponent) {
        return;
    }
    [self willChangeValueForKey:@"lComponent"];
    _lComponent = lComponent;
    [self didChangeValueForKey:@"lComponent"];
}

我们关闭了 -willChangeValueForKey: 和 -didChangeValueForKey: 的自动调用,然后我们手动调用他们。

如果我们在 accessor 方法之外改变实例对象(如 _lComponent ),我们要特别小心地和刚才一样封装 -willChangeValueForKey: 和 -didChangeValueForKey:。不过在多数情况下,我们只用 accessor 方法的话就可以了,这样代码会简洁很多。

KVO 和 context

有时我们会有理由不想用 KeyValueObserver 辅助类。创建另一个对象会有额外的性能开销。如果我们观察很多个键的话,这个开销可能会变得明显。

如果我们在实现一个类的时候把它自己注册为观察者的话

- (void)addObserver:(NSObject *)anObserver
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context

一个非常重要的点是我们要传入一个这个类唯一的 context。我们推荐把以下代码

static int const PrivateKVOContext;

写在这个类 .m 文件的顶端,然后我们像这样调用 API 并传入 PrivateKVOContext 的指针:

[otherObject addObserver:self forKeyPath:@"someKey" options:someOptions context:&PrivateKVOContext];

然后我们这样写 -observeValueForKeyPath:... 的方法:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == &PrivateKVOContext) {
        // 这里写相关的观察代码
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

这将确保我们写的子类都是正确的。如此一来,子类和父类都能安全的观察同样的键值而不会冲突。否则我们将会碰到难以 debug 的奇怪行为。

进阶 KVO

我们常常需要当一个值改变的时候更新 UI,但是我们也要在第一次运行代码的时候更新一次 UI。我们可以用 KVO 并添加NSKeyValueObservingOptionInitial 的选项 来一箭双雕地做好这样的事情。这将会让 KVO 通知在调用 -addObserver:forKeyPath:... 到时候也被触发。

之前和之后

当我们注册 KVO 通知的时候,我们可以添加 NSKeyValueObservingOptionPrior 选项,这能使我们在键值改变之前被通知。这和-willChangeValueForKey:被触发的时间相对应。

如果我们注册通知的时候附加了 NSKeyValueObservingOptionPrior 选项,我们将会收到两个通知:一个在值变更前,另一个在变更之后。变更前的通知将会在 change 字典中有不同的键。我们可以像以下这样区分通知是在改变之前还是之后被触发的:

if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
    // 改变之前
} else {
    // 改变之后
}

如果我们需要改变前后的值,我们可以在 KVO 选项中加入 NSKeyValueObservingOptionNew 和/或NSKeyValueObservingOptionOld

更简单的办法是用 NSKeyValueObservingOptionPrior 选项,随后我们就可以用以下方式提取出改变前后的值:

id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];

通常来说 KVO 会在 -willChangeValueForKey: 和 -didChangeValueForKey: 被调用的时候存储相应键的值。

索引

KVO 对一些集合类也有很强的支持,以下方法会返回集合对象:

-mutableArrayValueForKey:
-mutableSetValueForKey:
-mutableOrderedSetValueForKey:

我们将会详细解释这是怎么工作的。如果你使用这些方法,change 字典里会包含键值变化的类型(添加、删除和替换)。对于有序的集合,change 字典会包含受影响的 index。

KVO 和线程

一个需要注意的地方是,KVO 行为是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChange... 会触发 KVO 通知。

所以,当我们试图从其他线程改变属性值的时候我们应当十分小心,除非能确定所有的观察者都用线程安全的方法处理 KVO 通知。通常来说,我们不推荐把 KVO 和多线程混起来。如果我们要用多个队列和线程,我们不应该在它们互相之间用 KVO。

KVO 是同步运行的这个特性非常强大,只要我们在单一线程上面运行(比如主队列 main queue),KVO 会保证下列两种情况的发生:

首先,如果我们调用一个支持 KVO 的 setter 方法,如下所示:

self.exchangeRate = 2.345;

KVO 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到

其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到 -observe... 被调用之前,exchangeRate 的 accessor 方法都会返回同样的值。

KVC

最简单的 KVC 能让我们通过以下的形式访问属性:

@property (nonatomic, copy) NSString *name;

取值:

NSString *n = [object valueForKey:@"name"]

设定:

[object setValue:@"Daniel" forKey:@"name"];

值得注意的是这个不仅可以访问作为对象属性,而且也能访问一些标量(例如 int 和 CGFloat)和 struct(例如 CGRect)。Foundation 框架会为我们自动封装它们。举例来说,如果有以下属性:

@property (nonatomic) CGFloat height;

我们可以这样设置它:

[object setValue:@(20) forKey:@"height"]

KVC 允许我们用属性的字符串名称来访问属性,字符串在这儿叫做

键路径(Key Path)

KVC 同样允许我们通过关系来访问对象。假设 person 对象有属性 addressaddress 有属性 city,我们可以这样通过 person 来访问 city

[person valueForKeyPath:@"address.city"]

值得注意的是这里我们调用 -valueForKeyPath: 而不是 -valueForKey:

============= 也许你觉得上面很难理解,我将提供更简单的理解文章===============



KVO

概述

KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。

使用方法

系统框架已经支持KVO,所以程序员在使用的时候非常简单。

1. 注册,指定被观察者的属性,

2. 实现回调方法

3. 移除观察

实例

假设一个场景,股票的价格显示在当前屏幕上,当股票价格更改的时候,实时显示更新其价格。

1.定义DataModel,


@interface StockData : NSObject {  
    NSString * stockName;  
    float price;  
}  
@end  
@implementation StockData  
@end  

2.定义此model为Controller的属性,实例化它,监听它的属性,并显示在当前的View里边
- (void)viewDidLoad  
{  
    [super viewDidLoad];  
  
    stockForKVO = [[StockData alloc] init];  
    [stockForKVO setValue:@"searph" forKey:@"stockName"];  
    [stockForKVO setValue:@"10.0" forKey:@"price"];      
    [stockForKVO addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];  
  
    myLabel = [[UILabel alloc]initWithFrame:CGRectMake(100, 100, 100, 30 )];  
    myLabel.textColor = [UIColor redColor];  
    myLabel.text = [stockForKVO valueForKey:@"price"];  
    [self.view addSubview:myLabel];  
     
    UIButton * b = [UIButton buttonWithType:UIButtonTypeRoundedRect];  
    b.frame = CGRectMake(0, 0, 100, 30);  
    [b addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];  
    [self.view addSubview:b];  
  
}  

3.当点击button的时候,调用buttonAction方法,修改对象的属性
-(void) buttonAction  
{  
    [stockForKVO setValue:@"20.0" forKey:@"price"];  
} 

4. 实现回调方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context  
{  
    if([keyPath isEqualToString:@"price"])  
    {  
        myLabel.text = [stockForKVO valueForKey:@"price"];  
    }  
} 


KVO这种编码方式使用起来很简单,很适用与datamodel修改后,引发的UIVIew的变化这种情况,就像上边的例子那样,当更改属性的值后,监听对象会立即得到通知。

KVC

KVC是KeyValueCoding的简称,它是一种可以直接通过字符串的名字(key)来访问类属性的机制。而不是通过调用Setter、Getter方法访问。

当使用KVO、Core Data、CocoaBindings、AppleScript(Mac支持)时,KVC是关键技术。

使用方法

关键方法定义在:NSKeyValueCodingprotocol

KVC支持类对象和内建基本数据类型。

  获取值

valueForKey:,传入NSString属性的名字。

valueForKeyPath:,传入NSString属性的路径,xx.xx形式。

valueForUndefinedKey它的默认实现是抛出异常,可以重写这个函数做错误处理。

  修改值

setValue:forKey:

setValue:forKeyPath:

setValue:forUndefinedKey:

setNilValueForKey:当对非类对象属性设置nil时,调用,默认抛出异常。

  一对多关系成员的情况

mutableArrayValueForKey:有序一对多关系成员  NSArray

mutableSetValueForKey:无序一对多关系成员  NSSet

KVC

即是指 NSKeyValueCoding,一个非正式的 Protocol,提供一种机制来间接访问对象的属性。KVO 就是基于 KVC 实现的关键技术之一。

一个对象拥有某些属性。比如说,一个 Person 对象有一个 name 和一个 address 属性。以 KVC 说法,Person 对象分别有一个 value 对应他的 name 和 address 的 key。 key 只是一个字符串,它对应的值可以是任意类型的对象。从最基础的层次上看,KVC 有两个方法:一个是设置 key 的值,另一个是获取 key 的值。如下面的例子:


void changeName(Person *p, NSString *newName)
{
 
    // using the KVC accessor (getter) method
    NSString *originalName = [p valueForKey:@"name"];
 
    // using the KVC  accessor (setter) method.
    [p setValue:newName forKey:@"name"];
 
    NSLog(@"Changed %@'s name to: %@", originalName, newName);
 
}

现在,如果 Person 有另外一个 key 配偶(spouse),spouse 的 key 值是另一个 Person 对象,用 KVC 可以这样写:
void logMarriage(Person *p)
{
 
    // just using the accessor again, same as example above
    NSString *personsName = [p valueForKey:@"name"];
 
    // this line is different, because it is using
    // a "key path" instead of a normal "key"
    NSString *spousesName = [p valueForKeyPath:@"spouse.name"];
 
    NSLog(@"%@ is happily married to %@", personsName, spousesName);
 
}

key 与 key pat 要区分开来,key 可以从一个对象中获取值,而 key path 可以将多个 key 用点号 “.” 分割连接起来,比如:

[p valueForKeyPath:@ "spouse.name" ];

相当于这样……

[[p valueForKey:@ "spouse" ] valueForKey:@ "name" ];


“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值