Objective-C中的的KVO和KVC(转)

本文讲述了使用Cocoa框架中的KVC和KVO,实现观察者模式

KVC

键/值编码中的基本调用包括-valueForKey:-setValue:forKey:。以字符串的形式向对象发送消息,这个字符串是我们关注的属性的关键。
valueForKey:首先查找以键-key-isKey命名的getter方法。如果不存在getter方法(假如我们没有通过@synthesize提供存取方法),它将在对象内部查找名为_keykey的实例变量。
对于KVC,Cocoa自动放入和取出标量值(int,float和struct)放入NSNumber或NSValue中;当使用-setValue:ForKey:时,它自动将标量值从这些对象中取出。仅KVC具有这种自动包装功能,常规方法调用和属性语法不具备该功能。
-setValue:ForKey:的工作方式和-valueForKey:相同。它首先查找名称的setter方法,如果不存在setter方法,它将在类中查找名为_keykey的实例变量。

使用KVC访问属性的代价比直接使用存取方法要大,所以只在需要的时候才用。

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

1
@property (nonatomic, copy) NSString *name;

取值:

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

设定:

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

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

1
@property (nonatomic) CGFloat height;

我们可以这样设置它:

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

有关KVC的更多用法,参看下面的文章:

http://blog.csdn.net/omegayy/article/details/7381301
http://blog.csdn.net/wzzvictory/article/details/9674431
http://objccn.io/issue-7-3/

KVO

KVO是Cocoa提供的一种称为键-值观察的机制,对象可以通过它得到其他对象特性属性的变更通知。这种机制在MVC模式的场景中很重要,因为它让视图对象可以经由控制器层观察模型对象的变更。
这一机制基于NSKeyValueObserving非正式协议,Cocoa通过这个协议为所有遵守协议的对象提供了一种自动化的属性观察能力。要实现自动观察,参与KVO的对象需要符合KVC的要求和存取方法,也可以手动实现观察者通知,也可以两者都保留。

KVO是Cocoa框架使用观察者模式的一种途径。

设置一个属性的观察者需要三步,理解这些步骤可以更清楚的知道KVO的工作框图

  1. 首先看看你当前的场景如果使用KVO是否更妥当,比如,当一个实例的某个具体属性有任何变更的时候,另一个实例需要被通知。

比如,BankObject中的accountBalance属性有任何变更时,某个PersonObject对象都要觉察到。

  1. 这个PersonObject对象必须注册成为BankObject的accountBalance属性的观察者,可以通过发送addObserver:forKeyPath:options:context:消息来实现。

注意:addObserver:forKeyPath:options:context:方法在你指定的两个实例间建立联系,而不是在两个类之间。

  1. 为了回应变更通知,观察者必须实现observeValueForKeyPath:ofObject:change:context:方法。这个方法的实现决定了观察者如何回应变更通知。你可以在这个方法里自定义如何回应被观察属性的变更。

  1. 当一个被观察属性的值以符合KVO方式变更或者当它依赖的键变更时,observeValueForKeyPath:ofObject:change:context:方法会被自动执行。

Registering for Key-Value Observing

注册成为观察者

你可以通过发送addObserver:forKeyPath:options:context:消息来注册观察者

123456789101112
- (void)registerAsObserver {    /*     Register ‘inspector‘ to receive change notifications for the "openingBalance" property of     the ‘account‘ object and specify that both the old and new values of "openingBalance"     should be provided in the observe… method.     */    [account addObserver:inspector             forKeyPath:@"openingBalance"                 options:(NSKeyValueObservingOptionNew |                            NSKeyValueObservingOptionOld)                    context:NULL];}

inspector注册成为了account的观察者,被观察属性的KeyPath是@"openingBalance",也就是accountopeningBalance属性,NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld选项分别标识在观察者接收通知时change字典对应入口提供更改后的值和更改前的值。更简单的办法是用 NSKeyValueObservingOptionPrior 选项,随后我们就可以用以下方式提取出改变前后的值:(change是个字典,详细介绍请看下节)

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

我们常常需要当一个值改变的时候更新 UI,但是我们也要在第一次运行代码的时候更新一次 UI。我们可以用 KVO 并添加 NSKeyValueObservingOptionInitial 的选项 来一箭双雕地做好这样的事情。这将会让 KVO 通知在调用-addObserver:forKeyPath:... 到时候也被触发。
当我们注册 KVO 通知的时候,我们可以添加 NSKeyValueObservingOptionPrior 选项,这能使我们在键值改变之前被通知。这和-willChangeValueForKey:被触发的时间相对应。
如果我们注册通知的时候附加了 NSKeyValueObservingOptionPrior 选项,我们将会收到两个通知:一个在值变更前,另一个在变更之后。变更前的通知将会在 change 字典中有不同的键。

context是一个指针,当observeValueForKeyPath:ofObject:change:context:方法执行时context会提供给观察者。context可以是C指针或者一个对象引用,既可以当作一个唯一的标识来分辨被观察的变更,也可以向观察者提供数据。

接收变更通知

当被观察的属性变更时,观察者会接到observeValueForKeyPath:ofObject:change:context:消息,所有的观察者都必须实现这个方法。
观察者会被提供触发通知的对象和keyPath,一个包含变更详细信息的字典,还有一个注册观察者时提供的context指针。

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

    if ([keyPath isEqual:@"openingBalance"]) {        [openingBalanceInspectorField setObjectValue:            [change objectForKey:NSKeyValueChangeNewKey]];    }    /*     Be sure to call the superclass‘s implementation *if it implements it*.     NSObject does not implement the method.     */    [super observeValueForKeyPath:keyPath                         ofObject:object                           change:change                           context:context];}

关于change参数,它是一个字典,有五个常量作为它的键:

12345
NSString *const NSKeyValueChangeKindKey;  NSString *const NSKeyValueChangeNewKey;  NSString *const NSKeyValueChangeOldKey;  NSString *const NSKeyValueChangeIndexesKey;  NSString *const NSKeyValueChangeNotificationIsPriorKey;

NSKeyValueChangeKindKey
指明了变更的类型,值为“NSKeyValueChange”枚举中的某一个,类型为NSNumber。

1234567
enum {   NSKeyValueChangeSetting = 1,   NSKeyValueChangeInsertion = 2,   NSKeyValueChangeRemoval = 3,   NSKeyValueChangeReplacement = 4};typedef NSUInteger NSKeyValueChange;

NSKeyValueChangeNewKey
如果 NSKeyValueChangeKindKey的值为 NSKeyValueChangeSetting,并且 NSKeyValueObservingOptionNew选项在注册观察者时也指定了,那么这个键的值就是属性变更后的新值。
对于 NSKeyValueChangeInsertion或者NSKeyValueChangeReplacement,如果 NSKeyValueObservingOptionNew选项在注册观察者时也指定了,这个键的值是一个数组,其包含了插入或替换的对象。
NSKeyValueChangeOldKey
如果 NSKeyValueChangeKindKey的值为 NSKeyValueChangeSetting,并且 NSKeyValueObservingOptionOld选项在注册观察者时也指定了,那么这个键的值就是属性变更前的旧值。
对于  NSKeyValueChangeRemoval 或者NSKeyValueChangeReplacement,如果 NSKeyValueObservingOptionOld选项在注册观察者时也指定了,这个键的值是一个数组,其包含了被移除或替换的对象。
NSKeyValueChangeIndexesKey
如果 NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertionNSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index。
NSKeyValueChangeNotificationIsPriorKey
如果注册观察者时NSKeyValueObservingOptionPrior选项被指明了,此通知会在变更发生前被发出。其类型为NSNumber,包含的值为YES。我们可以像以下这样区分通知是在改变之前还是之后被触发的:

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

移除观察者

你可以通过发送removeObserver:forKeyPath:消息来移除观察者,你需要指明观察对象和路径。

123
- (void)unregisterForChangeNotification {    [observedObject removeObserver:inspector forKeyPath:@"openingBalance"];}

上面的代码将openingBalance属性的观察者inspector移除,移除后观察者再也不会收到observeValueForKeyPath:ofObject:change:context:消息。
在移除观察者之前,如果context是一个对象的引用,那么必须保持对它的强引用直到观察者被移除。

KVO Compliance(KVO兼容)

有两种方法可以保证变更通知被发出。自动发送通知是NSObject提供的,并且一个类中的所有属性都默认支持,只要是符合KVO的。一般情况你使用自动变更通知,你不需要写任何代码。
人工变更通知需要些额外的代码,但也对通知发送提供了额外的控制。你可以通过重写子类automaticallyNotifiesObserversForKey:方法的方式控制子类一些属性的自动通知。

Automatic Change Notification(自动通知)

下面代码中的方法都能导致KVO变更消息发出

12345678910111213
// Call the accessor method.[account setName:@"Savings"];

// Use setValue:forKey:.[account setValue:@"Savings" forKey:@"name"];

// Use a key path, where ‘account‘ is a kvc-compliant property of ‘document‘.[document setValue:@"Savings" forKeyPath:@"account.name"];

// Use mutableArrayValueForKey: to retrieve a relationship proxy object.Transaction *newTransaction = <#Create a new transaction for the account#>;NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];[transactions addObject:newTransaction];

Manual Change Notification(手动通知)

下面的代码为openingBalance属性开启了人工通知,并让父类决定其他属性的通知方式。

1234567891011
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {

    BOOL automatic = NO;    if ([theKey isEqualToString:@"openingBalance"]) {        automatic = NO;    }    else {        automatic = [super automaticallyNotifiesObserversForKey:theKey];    }    return automatic;}

要实现人工观察者通知,你要执行在变更前执行willChangeValueForKey:方法,在变更后执行didChangeValueForKey:方法:

12345
- (void)setOpeningBalance:(double)theBalance {    [self willChangeValueForKey:@"openingBalance"];    _openingBalance = theBalance;    [self didChangeValueForKey:@"openingBalance"];}

为了使不必要的通知最小化我们应该在变更前先检查一下值是否变了:

1234567
- (void)setOpeningBalance:(double)theBalance {    if (theBalance != _openingBalance) {        [self willChangeValueForKey:@"openingBalance"];        _openingBalance = theBalance;        [self didChangeValueForKey:@"openingBalance"];    }}

如果一个操作导致了多个键的变化,你必须嵌套变更通知:

12345678
- (void)setOpeningBalance:(double)theBalance {    [self willChangeValueForKey:@"openingBalance"];    [self willChangeValueForKey:@"itemChanged"];    _openingBalance = theBalance;    _itemChanged = _itemChanged+1;    [self didChangeValueForKey:@"itemChanged"];    [self didChangeValueForKey:@"openingBalance"];}

在to-many关系操作的情形中,你不仅必须表明key是什么,还要表明变更类型和影响到的索引。变更类型是一个 NSKeyValueChange值,被影响对象的索引是一个 NSIndexSet对象。
下面的代码示范了在to-many关系transactions对象中的删除操作:

123456789
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {    [self willChange:NSKeyValueChangeRemoval        valuesAtIndexes:indexes forKey:@"transactions"];

    // Remove the transaction objects at the specified indexes.

    [self didChange:NSKeyValueChangeRemoval        valuesAtIndexes:indexes forKey:@"transactions"];}

Registering Dependent Keys(注册依赖键)

有一些属性的值取决于一个或者多个其他对象的属性值,一旦某个被依赖的属性值变了,依赖它的属性的变化也需要被通知。

To-one Relationships

要自动触发 to-one关系,有两种方法:重写keyPathsForValuesAffectingValueForKey:方法或者定义名称为keyPathsForValuesAffecting<Key>的方法。

例如一个人的全名是由姓氏和名子组成的:

123
- (NSString *)fullName {    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];}

一个观察fullName的程序在firstName或者lastName变化时也应该接收到通知。

一种解决方法是重写keyPathsForValuesAffectingValueForKey:方法来表明fullname属性是依赖于firstnamelastname的:

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

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    if ([key isEqualToString:@"fullName"]) {        NSArray *affectingKeys = @[@"lastName", @"firstName"];        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];    }    return keyPaths;}

相当于在影响fullName值的keypath中新加了两个key:lastNamefirstName,很容易理解。

另一种实现同样结果的方法是实现一个遵循命名方式为keyPathsForValuesAffecting<Key>的类方法,<Key>是依赖于其他值的属性名(首字母大写),用上面代码的例子来重新实现一下:

123
+ (NSSet *)keyPathsForValuesAffectingFullName {    return [NSSet setWithObjects:@"lastName", @"firstName", nil];}

有时在类别中我们不能添加keyPathsForValuesAffectingValueForKey:方法,因为不能再类别中重写方法,所以这时可以实现keyPathsForValuesAffecting<Key>方法来代替。

注意:你不能在keyPathsForValuesAffectingValueForKey:方法中设立to-many关系的依赖,相反,你必须观察在to-many集合中的每一个对象中相关的属性并通过亲自更新他们的依赖来回应变更。下一节将会讲述对付此情形的策略。

To-many Relationships

keyPathsForValuesAffectingValueForKey:方法不支持包含to-many关系的keypath。比如,假如你有一个Department类,它有一个针对Employee类的to-many关系(雇员),Employee类有salary属性。你希望Department类有一个totalSalary属性来计算所有员工的薪水,也就是在这个关系中DepartmenttotalSalary依赖于所有Employeesalary属性。你不能通过实现keyPathsForValuesAffectingTotalSalary方法并返回employees.salary

有两种解决方法:

  1. 你可以用KVO将parent(比如Department)作为所有children(比如Employee)相关属性的观察者。你必须在把child添加或删除到parent时也把parent作为child的观察者添加或删除。在observeValueForKeyPath:ofObject:change:context:方法中我们可以针对被依赖项的变更来更新依赖项的值:
12345678910111213141516171819202122232425
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    if (context == totalSalaryContext) {        [self updateTotalSalary];    }    else    // deal with other observations and/or invoke super...}

- (void)updateTotalSalary {    [self setTotalSalary:[self valueForKeyPath:@"[email protected]"]];}

- (void)setTotalSalary:(NSNumber *)newTotalSalary {

    if (totalSalary != newTotalSalary) {        [self willChangeValueForKey:@"totalSalary"];        _totalSalary = newTotalSalary;        [self didChangeValueForKey:@"totalSalary"];    }}

- (NSNumber *)totalSalary {    return _totalSalary;}

2.如果你在使用Core Data,你可以在应用的notification center中将parent注册为它的 managed object context的观察者,parent应该回应相应的变更通知,这些通知是children以类似KVO的形式发出的。

其实这也是Objective-C中利用Cocoa实现观察者模式的另一种途径:NSNotificationCenter

调试KVO

你可以在 lldb 里查看一个被观察对象的所有观察信息。

1
(lldb) po [observedObject observationInfo]

这会打印出有关谁观察谁之类的很多信息。

这个信息的格式不是公开的,我们不能让任何东西依赖它,因为苹果随时都可以改变它。不过这是一个很强大的排错工具。

转载地址:http://yulingtianxia.com/blog/2014/05/12/objective-czhong-de-kvche-kvo/

时间: 2024-11-03 01:34:05

Objective-C中的的KVO和KVC(转)的相关文章

iOS开发——实用篇&amp;KVO与KVC详解

KVO与KVC详解 由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby.Python的动态特性,例如动态类型.动态加载.动态绑定等.今天我们着重介绍ObjC中的键值编码(KVC).键值监听(KVO)特性: 键值编码KVC 键值监听KVO 键值编码KVC 我们知道在C#中可以通过反射读写一个对象的属性,有时候这种方式特别方便,因为你可以利用字符串的方式去动态控制一个对象.其实由于ObjC的语言特性,你根部不必进行任何操作就可以进行属性的动态读写,这种方式就是Key Valu

KVO 与 KVC 区别

还长时间 没来了  今天分享一下 个人总结的 KCO  KVC 笔记: (如有错误,请速速联系我  愿听你的建议!) KVO 与 KVC 区别: KVO 主要用于监听属性属性改变 KVC 主要用于对某一对象的成员变量赋值 KVO:    运用KVO 监听成员属性 时   想要监听哪个 就对哪个属性监听 // KVO 监听属性改变 [item addObserver:self forKeyPath:@"one" options:0 context:nil]; [item addObser

iOS: 消息通信中的Notification&amp;KVO

iOS: 消息通信中的Notification&KVO 在 iOS: MVC 中,我贴了张经典图: 其中的Model向Controller通信的Noification&KVO为何物呢? 在功能上说,delegate.Notification以及KVO的功能类似,都是作用于 OC中对象 的消息通信.但三者的使用场景是不同的. 简单的说Delegate是一种回掉函数,更多的用在一对一的场合,可参考 iphone:delegate机制 : Notification 用得较少,使用Notifica

iOS之KVO和KVC

概述 由于ObjC主要基于Smalltalk进行设计,因此它有很多类似于Ruby.Python的动态特性,例如动态类型.动态加载.动态绑定等.今天我们着重介绍ObjC中的键值编码(KVC).键值监听(KVO)特性: 键值编码KVC 键值监听KVO 键值编码KVC 我们知道在C#中可以通过反射读写一个对象的属性,有时候这种方式特别方便,因为你可以利用字符串的方式去动态控制一个对象.其实由于ObjC的语言特性,你根部不必进行任何操作就可以进行属性的动态读写,这种方式就是Key Value Codin

objective C中的字符串(三)

holydancer原创,如需转载,请在显要位置注明: 转自holydancer的CSDN专栏,原文地址:http://blog.csdn.net/holydancer/article/details/7343561 objective C中的字符串操作 在OC中创建字符串时,一般不使用C的方法,因为C将字符串作为字符数组,所以在操作时会有很多不方便的地方,在Cocoa中NSString集成的一些方法,可以很方便的操作字符串,下面举几个例子: 1.创建: 直接利用等号赋值 NSString *

objective C中继承、协议、分类和多态的实现

第一.objective C中继承的实现 在oc中只有实例变量会有权限控制,实例方法和类方法是没有权限控制的,这点与c++不同,OC默认的是protected,并且在声明权限控制时,没有分号 在OC中可以像C++一样用指针运算法来访问实例变量 Rectangle.h 文件代码: #import <Foundation/Foundation.h> @interface Rectangle : NSObject { int _width; int _height; } @property (non

objective C中的字符串

holydancer原创,如需转载,请在显要位置注明: 转自holydancer的CSDN专栏,原文地址:http://blog.csdn.net/holydancer/article/details/7343561 objective C中的字符串操作 在OC中创建字符串时,一般不使用C的方法,因为C将字符串作为字符数组,所以在操作时会有很多不方便的地方,在Cocoa中NSString集成的一些方法,可以很方便的操作字符串,下面举几个例子: 1.创建: 直接利用等号赋值 NSString *

设计模式之观察者模式(关于OC中的KVO(Observer)\KVC\NSNotification)

学习了这么久的设计模式方面的知识,最大的感触就是,设计模式不能脱离语言特性.近段时间所看的两本书籍,<大话设计模式>里面的代码是C#写的,有一些设计模式实现起来也是采用了C#的语言特性(C#的API,抽象类,在OC中是没有抽象类.没有多继承关系),<设计模式之禅>里面的代码是JAVA写的,与OC差距也是比较大. 但是我想,这些都不是问题,学习设计模式主要学习的是其中的思想,并将之改造成自己所熟悉语言的模式,大同小异.所需要注意的是,在学习的过程中,将之与语言结合起来,多思考.多实践

Objective - C 中的KVC(一)(视图、便携、易懂、原创纯手打 定制版)

KVC的使用 1.KVC 全称 key valued coding 键值编码 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性.JAVA,C#都有这个机制.ObjC也有,所以你根部不必进行任何操作就可以进行属性的动态读写,就是KVC. KVC的操作方法由NSKeyValueCoding提供,而他是NSObject的类别,也就是说ObjC中几乎所有的对象都支持KVC操作. 2.常用方法 获取值的方法 valueForKey