IOS开发之旅-KVO
在设计模式中,有一种模式称为观察者模式,Objective-c也提供了类似的机制,简称为KVO【Key-Value Observing】。当被观察者的属性改变时立即通知观察者触发响应的行为。
在KVO中,首先被观察者与观察者应该先建立关系,当被观察的特定属性改变时,立刻通知观察者,建立联系调用如下方法:
/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong. */ - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
对参数options说明下,可以通过|操作符进行或操作,NSKeyValueObservingOptions定义如下:
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) { NSKeyValueObservingOptionNew = 0x01, //作为变更信息的一部分发送新值 NSKeyValueObservingOptionOld = 0x02, //作为变更信息的一部分发送旧值 NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04, //在观察者注册时发送一个初始更新 NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08 //在变更前后分别发送变更,而不只在变更后发送一次 };
addObserver的context参数与observeValueForKeyPath的context参数指向同一个地址,进行额外参数的传递。
当不在需要观察者监听被观察者的属性变化时,必须移除这种监听关系,否则程序会抛出异常,调用如下方法移除监听关系:
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context NS_AVAILABLE(10_7, 5_0); - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
当观察者接受到被观察者属性改变的通知时,立即调用特定的方法以示响应,观察者必须实现如下方法:
/* Given that the receiver has been registered as an observer of the value at a key path relative to an object, be notified of a change to that value. The change dictionary always contains an NSKeyValueChangeKindKey entry whose value is an NSNumber wrapping an NSKeyValueChange (use -[NSNumber unsignedIntegerValue]). The meaning of NSKeyValueChange depends on what sort of property is identified by the key path: - For any sort of property (attribute, to-one relationship, or ordered or unordered to-many relationship) NSKeyValueChangeSetting indicates that the observed object has received a -setValue:forKey: message, or that the key-value coding-compliant set method for the key has been invoked, or that a -willChangeValueForKey:/-didChangeValueForKey: pair has otherwise been invoked. - For an _ordered_ to-many relationship, NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, and NSKeyValueChangeReplacement indicate that a mutating message has been sent to the array returned by a -mutableArrayValueForKey: message sent to the object, or sent to the ordered set returned by a -mutableOrderedSetValueForKey: message sent to the object, or that one of the key-value coding-compliant array or ordered set mutation methods for the key has been invoked, or that a -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey: pair has otherwise been invoked. - For an _unordered_ to-many relationship (introduced in Mac OS 10.4), NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, and NSKeyValueChangeReplacement indicate that a mutating message has been sent to the set returned by a -mutableSetValueForKey: message sent to the object, or that one of the key-value coding-compliant set mutation methods for the key has been invoked, or that a -willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: pair has otherwise been invoked. For any sort of property, the change dictionary contains an NSKeyValueChangeNewKey entry if NSKeyValueObservingOptionNew was specified at observer registration time, it's the right kind of change, and this isn't a prior notification. The change dictionary contains an NSKeyValueChangeOldKey if NSKeyValueObservingOptionOld was specified and it's the right kind of change. See the comments for the NSKeyValueObserverNotification informal protocol methods for what the values of those entries can be. For an _ordered_ to-many relationship, the change dictionary always contains an NSKeyValueChangeIndexesKey entry whose value is an NSIndexSet containing the indexes of the inserted, removed, or replaced objects, unless the change is an NSKeyValueChangeSetting. If NSKeyValueObservingOptionPrior (introduced in Mac OS 10.5) was specified at observer registration time, and this notification is one being sent prior to a change as a result, the change dictionary contains an NSKeyValueChangeNotificationIsPriorKey entry whose value is an NSNumber wrapping YES (use -[NSNumber boolValue]). context is always the same pointer that was passed in at observer registration time. */ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
对于observeValueForKeyPath的字典类型的change参数的key可能包含如下的值,具体包含什么key,是由调用addObserver时设置的options参数的值
FOUNDATION_EXPORT NSString *const NSKeyValueChangeKindKey; FOUNDATION_EXPORT NSString *const NSKeyValueChangeNewKey; FOUNDATION_EXPORT NSString *const NSKeyValueChangeOldKey; FOUNDATION_EXPORT NSString *const NSKeyValueChangeIndexesKey; FOUNDATION_EXPORT NSString *const NSKeyValueChangeNotificationIsPriorKey NS_AVAILABLE(10_5, 2_0);
NSKeyValueChangeKindKey可能取如下的枚举值:
/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information. */ typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, NSKeyValueChangeInsertion = 2, NSKeyValueChangeRemoval = 3, NSKeyValueChangeReplacement = 4 };
以上介绍只针对单属性的监听,有时候可能会遇到组合属性的监听,比如说一个用户的fullName由firstName、lastName构成,那么不管是firstName还是lastName被改变了,fullName都会跟随改变,如果观察者监听了fullName,那么firstName、lastName改变时候都应该触发观察者的observeValueForKeyPath方法,那么如何在firstName或者lastName改变的时候自动触发观察者的observeValueForKeyPath方法?观察者必须实现如下方法
/* Return a set of key paths for properties whose values affect the value of the keyed property. When an observer for the key is registered with an instance of the receiving class, KVO 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<Key>, and returns the result of invoking that method if it is found. So, any such method must return an NSSet too. 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. This method and KVO's automatic use of it comprise a dependency mechanism that you can use instead of sending -willChangeValueForKey:/-didChangeValueForKey: messages for dependent, computed, properties. 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 invoke 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). You can't really override this method when you add a computed property to an existing class using a category, because you're not supposed to override methods in categories. In that case, implement a matching +keyPathsForValuesAffecting<Key> to take advantage of this mechanism. */ + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
或者
+(NSSet*)keyPathsForValuesAffecting<Key>
下面用生活中得一个例子来演示,当用户银行中得余额发生改变时应当立即通知用户改变情况,
KVOBankObject:
#import <Foundation/Foundation.h> @interface BankObject : NSObject { int _accountBalance; } @property int accountBalance; -(id)initWithAccountBalance:(int)accountBalance; @end
#import "KVOBankObject.h" @implementation BankObject -(id)initWithAccountBalance:(int)accountBalance { if(self = [super init]) { self.accountBalance = accountBalance; } return self; } @end
KVOPersonObject:
#import <Foundation/Foundation.h> @interface PersonObject : NSObject @property (nonatomic,strong) NSString* firstName; @property (nonatomic,strong) NSString* lastName; -(id)initWithFirstName:(NSString*)firstName LastName:(NSString*)lastName; -(NSString*)fullName; @end #import "KVOPersonObject.h" #import "KVOBankObject.h" @implementation PersonObject -(id)initWithFirstName:(NSString *)firstName LastName:(NSString *)lastName { if(self = [super init]) { self.firstName = firstName; self.lastName = lastName; } return self; } -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([object isKindOfClass:[BankObject class]]) { if([keyPath isEqualToString:NSStringFromSelector(@selector(accountBalance))]) { NSLog(@"%@",change); } } if ([object isKindOfClass:[PersonObject class]]) { if([keyPath isEqualToString:NSStringFromSelector(@selector(fullName))]) { NSLog(@"%@",change); } } } -(NSString*)fullName { return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName]; } +(NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if([key isEqualToString:NSStringFromSelector(@selector(fullName))]) { NSArray *affectingKeys = @[NSStringFromSelector(@selector(lastName)),NSStringFromSelector(@selector(firstName))]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; } +(NSSet *)keyPathsForValuesAffectingFullName { return [NSSet setWithObjects:NSStringFromSelector(@selector(lastName)),NSStringFromSelector(@selector(firstName)), nil]; } @end
调用示例
#import <Foundation/Foundation.h> #import "KVOBankObject.h" #import "KVOPersonObject.h" int main(int argc, const char * argv[]) { @autoreleasepool { PersonObject *personObj = [[PersonObject alloc]init]; BankObject *bankObj = [[BankObject alloc]initWithAccountBalance:1]; [bankObj addObserver:personObj forKeyPath:@"accountBalance" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; bankObj.accountBalance = 2; [bankObj removeObserver:personObj forKeyPath:@"accountBalance"]; } return 0; }
因为options : NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew,所以输出属性的旧值与新值,如下所示:
{ kind = 1; new = 2; old = 1; }
如果设置options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial。NSKeyValueObservingOptionInitial在观察者注册时发送一个初始更新,输出如下:
//注册时发送一个更新请求 { kind = 1; new = 1; } //值改变之后发送一个更新请求 { kind = 1; new = 2; old = 1; }
如果设置options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior。NSKeyValueObservingOptionPrior在变更前后分别发送变更,而不只在变更后发送一次。输出如下:
//变更前发送一个请求
{ kind = 1; notificationIsPrior = 1; old = 1; }
//变更后发送一个请求 { kind = 1; new = 2; old = 1; }
下面演示组合属性fullName的监听,当firstName、lastName改变时,触发监听方法
#import <Foundation/Foundation.h> #import "KVOBankObject.h" #import "KVOPersonObject.h" int main(int argc, const char * argv[]) { @autoreleasepool { PersonObject *personObj = [[PersonObject alloc]initWithFirstName:@"quan" LastName:@"long"]; [personObj addObserver:personObj forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil]; personObj.firstName = @"pstune"; //触发一次监听 personObj.lastName = @".com"; //触发一次监听 [personObj removeObserver:personObj forKeyPath:@"fullName"]; /* BankObject *bankObj = [[BankObject alloc]initWithAccountBalance:1]; [bankObj addObserver:personObj forKeyPath:@"accountBalance" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior context:nil]; bankObj.accountBalance = 2; [bankObj removeObserver:personObj forKeyPath:@"accountBalance"]; */ } return 0; }
以上算是对KVO的简单总结,如有不正确的地方,请给我留言。