iOS KVO 键值观察

Posted Xiejunyi12

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS KVO 键值观察相关的知识,希望对你有一定的参考价值。

观察 model 对象的变化

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

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

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

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

@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;

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

@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<键名>;

在我们的例子中如下:

+ (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 方法,这些依赖关系仍然能正常工作。

看完了上面objc的内容,让我来再深入分析一下

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

Discussion

When an observer for the key is registered with an instance of the receiving class, key-value observing itself automatically observes all of the key paths for the same instance, and sends change notifications for the key to the observer when the value for any of those key paths changes.

The default implementation of this method searches the receiving class for a method whose name matches the pattern +keyPathsForValuesAffecting, and returns the result of invoking that method if it is found. Any such method must return an NSSet. If no such method is found, an NSSet that is computed from information provided by previous invocations of the now-deprecated setKeys:triggerChangeNotificationsForDependentKey: method is returned, for backward binary compatibility.

You can override this method when the getter method of one of your properties computes a value to return using the values of other properties, including those that are located by key paths. Your override should typically call super and return a set that includes any members in the set that result from doing that (so as not to interfere with overrides of this method in superclasses).

大意就是,当一个观察者被注册的时候,键值观察会观察该类所有的实例的指定键值对,当键值对改变的时候发送通知。该方法的默认实现模式为 + (NSSet )keyPathsForValuesAffectingValueForKey:(NSString )key;
你可以重写这些getter方法,返回自定义的影响属性。

+ (NSSet *)keyPathsForValuesAffectingColor

    return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil];

观察变化

我们把视图控制器注册为观察者来接收 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;

方法解析

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

参数
anObserver
要注册KVO通知的对象。 观察者必须实现键值观察方法observeValueForKeyPath:ofObject:change:context :

keyPath
相对于数组的关键路径,要观察的属性。 此值不能为零。

options
NSKeyValueObservingOptions值的组合,指定包含在观察通知中的内容。

context
在ObserValueForKeyPath:ofObject:change:context:中传递给观察者的任意数据。

讨论
NSArray对象不可观察,因此当在NSArray对象上调用时,此方法引发异常。 不要观察数组,而要观察数组是相关对象集合的一对多关系。

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context;

Parameters

keyPath
The key path, relative to object, to the value that has changed.

object
The source object of the key path keyPath.

change
A dictionary that describes the changes that have been made to the value of the property at the key path keyPath relative to object. Entries are described in Change Dictionary Keys.

context
The value that was provided when the observer was registered to receive key-value observation notifications.

参数

keyPath
相对于对象的键路径,已更改的值。

object
键路径keyPath的源对象。

change
描述对关键路径keyPath相对于对象的属性的值所做的更改的字典。 条目在更改字典键中描述。

context
注册观察者以接收键值观察通知时提供的值。

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

NSArray对象不可观察,因此当在NSArray对象上调用时,此方法引发异常。 不要观察数组,而要观察数组是相关对象集合的一对多关系。

通知

当我们观察的key 的 value 放生改变shi,会调用以下方法:

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

和:

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

进阶KVO

options

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) 
    //如果我们需要改变前后的值,我们可以在 KVO 选项中加入 NSKeyValueObservingOptionNew 和/或 NSKeyValueObservingOptionOld。
    //更简单的办法是用 NSKeyValueObservingOptionPrior 选项,随后我们就可以用以下方式提取出改变前后的值:
    //id oldValue = change[NSKeyValueChangeOldKey];
    //id newValue = change[NSKeyValueChangeNewKey];
    NSKeyValueObservingOptionNew = 0x01,
    NSKeyValueObservingOptionOld = 0x02,
    NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,//初始化(addObserver:)时,KVO就被触发
    NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08//这能使我们在键值改变之前被通知,这和-willChangeValueForKey:被触发的时间相对应
;

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

KVO 的实现

当你观察一个对象时,一个新的类会动态被创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。自然,重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象值的更改。最后把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
原来,这个中间类,继承自原本的那个类。不仅如此,Apple 还重写了 -class 方法,企图欺骗我们这个类没有变,就是原本那个类。更具体的信息,去跑一下 Mike Ash 的那篇文章里的代码就能明白,这里就不再重复。

以上是关于iOS KVO 键值观察的主要内容,如果未能解决你的问题,请参考以下文章

iOSKVC 与 KVO

Rx 键值观察KVO的使用

Swift 中是不是提供键值观察 (KVO)?

iOS 设计模式(五)-KVO 详解

iOS KVO探索

iOS KVO探索