为了理解
KVO
,首先需要了解 Key-Value Coding。
kvo
Key-value observing
提供途径允许将其他对象的特定属性的更改通知给对象。可以通过Key-value observing
观察到包括简单
属性,一对一关系
和一对多关系
的属性。一对多关系的观察者被告知所做更改的类型,以及更改涉及哪些对象。
注意: 虽然 UIKit 框架的类一般不支持 KVO,但仍然可以在应用程序的自定义对象中实现它,包括自定义视图。
KVO
的编程思想
响应式编程
KVO
的使用
- 被观察者添加观察者addObserver:forKeyPath:options:context:
- 观察者实现监听方法observeValueForKeyPath:ofObject:change:context:
- 最后,被观察者移除监听removeObserver:forKeyPath:`
NSKeyValueChangeKey
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
NSKeyValueChange
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
NSKeyValueSetMutationKind
typedef NS_ENUM(NSUInteger, NSKeyValueSetMutationKind) {
NSKeyValueUnionSetMutation = 1,
NSKeyValueMinusSetMutation = 2,
NSKeyValueIntersectSetMutation = 3,
NSKeyValueSetSetMutation = 4
};
Registering for Key-Value Observing
参数Options
options
参数(指定为选项常量的按位或)既会影响通知中提供的更改字典的内容,又会影响生成通知的方式。
参数context
context
参数,上下文,void *
作用
一种更安全,更可扩展的方法是使用上下文确保您收到的通知是发给观察者的,而不是超类的。
实现
-
变量
PersonNameContext
static void *PersonNameContext = &PersonNameContext;
-
添加观察者时配置
context
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:PersonNameContext];
-
观察时判断
static void *PersonNameContext = &PersonNameContext; - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == PersonNameContext) { } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } }
Receiving Notification of a Change
-
当对象的观察属性的值更改时,观察者将收到一条
ObservValueForKeyPath:ofObject:change:context:
消息。所有观察者都必须实现此方法。 -
NSKeyValueChangeKindKey
提供有关发生的更改类型的信息。如果所观察对象的值已更改,则NSKeyValueChangeKindKey
将返回NSKeyValueChangeSetting
。 -
根据注册观察者时指定的选项,更改字典中的
NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
包含更改前后的属性值。 -
如果属性是对象,则直接提供值。如果属性是标量或
C
结构,则将值包装在NSValue
对象中。 -
如果观察到的属性是一对多关系,则
NSKeyValueChangeKindKey
还通过分别返回NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
来插入,移除或替换关系中的对象。 -
在任何情况下,观察者均应在无法识别上下文(或在简单情况下,是任何键路径)时始终调用父类的
observeValueForKeyPath:ofObject:change:context
的实现,因为这意味着父类已针对通知。 -
在任何情况下,观察者均应在无法识别上下文(或在简单情况下,是任何键路径)时始终调用父类的
observeValueForKeyPath:ofObject:change:context
的实现,因为这意味着超类已针对通知。- 如果在注册观察者时指定了
NULL
上下文,则将通知的键路径与要观察的键路径进行比较,以确定发生了什么变化。 - 如果为所有观察到的关键路径使用了单个上下文,则首先要根据通知的上下文进行测试,然后找到匹配项,然后使用关键路径字符串比较来确定具体更改的内容。
- 如果为每个键路径提供了唯一的上下文,如此处所示,则一系列简单的指针比较会同时告诉您通知是否针对此观察者,如果是,则更改了哪个键路径。
- 如果在注册观察者时指定了
-
如果通知传播到类层次结构的顶部,则
NSObject
会引发NSInternalInconsistencyException
,因为这是编程错误:子类无法使用为其注册的通知通常
NSInternalInconsistencyException
,是由于未移除观察者导致的
Removing an Object as an Observer
通过向被观察对象发送一条removeObserver:forKeyPath:context:
消息来删除键值观察者,并指定观察对象,键路径和上下文。
[self.person removeObserver:self forKeyPath:@"name" context:PersonNameContext];
删除观察者时,请记住以下几点:
- 如果尚未注册,移除观察者会导致
NSRangeException
; -
removeObserver:forKeyPath:context:
与addObserver:forKeyPath:options:context:
一一对应; - 请将
removeObserver:forKeyPath:context:
调用在try / catch
块内处理潜在的异常。
KVO Compliance
Manual Change Notification
手动和自动通知不是互斥的。除了已经发布的自动通知之外,还可以发布手动通知。
通常,可能希望完全控制特定属性的通知。在这种情况下,将覆盖NSObject实现的automaticNotifyObserversForKey:
。对于要排除其自动通知的属性,automaticNotifyObserversForKey
的类实现应返回NO
。
automaticallyNotifiesObserversForKey:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
Example accessor method implementing manual notification
要实现手动观察者通知,请在更改值之前调用willChangeValueForKey:
,在更改值之后调用didChangeValueForKey:
。
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
Implementation of manual observer notification in a to-many relationship
在有序对多
关系的情况下,不仅必须指定已更改的键,还必须指定更改的类型和所涉及对象的索引。更改的类型是NSKeyValueChange
,它指定NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
。受影响的对象的索引作为NSIndexSet
对象传递。
- (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
fullName
依赖于firstName
和lastName
,当firstName
或lastName
属性更改时,必须通知观察fullName
属性的应用程序,因为它们会影响该属性的值。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
路径观察
-
重写
keyPathsForValuesAffectingValueForKey:
,设置依赖的从属键firstName
和lastName
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; if ([key isEqualToString:@"fullName"]) { NSArray *affectingKeys = @[@"lastName", @"firstName"]; keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys]; } return keyPaths; }
-
通过实现遵循命名约定
keyPathsForValuesAffecting <Key>
的类方法来获得相同的结果,其中<Key>
是依赖于值的属性名称(首字母大写)。添加到类别时可以使用此实现方式
+ (NSSet *)keyPathsForValuesAffectingFullName { return [NSSet setWithObjects:@"lastName", @"firstName", nil]; }
无法通过实现
keyPathsForValuesAffectingValueForKey:
来建立对多关系的依赖关系。必须观察“to-many
”集合中每个对象的适当属性,并通过自己更新相关键来响应其值的更改。
数组观察
mutableArrayValueForkey:
监听数组的变化
self.person.dataArray = [NSMutableArray array];
[self.person addObserver:self forKeyPath:@"dataArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:PersonNameContext];
用KVC的方式变更属性
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"1"];
监听结果
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == PersonNameContext) {
NSLog(@"change%@",change);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
输出
change{
indexes = "<_NSCachedIndexSet: 0x600003665d80>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
1
);
}
KVO
底层原理
KVO
无法监听对类的成员变量赋值
KVO
是使用isa-swizzling
的技术实现的
- 顾名思义,
isa
指针指向维护分配表的对象的类。该分派表实质上包含指向该类实现的方法的指针以及其他数据。 - 在为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。结果,
isa
指针的值不一定反映实例的实际类。 - 永远不应依靠
isa
指针来确定类成员身份。相反,应该使用class
方法来确定对象实例的类。
调试添加及移除观察者的前后
-
在添加观察者前以及添加观察者后,分别使用
添加观察者前后调试.pngobject_getClassName()
可得下图,NSKVONotifying_Person
为Person
的子类(派生类)在添加观察者后,查看
NSKVONotifying_Person
和Person的方法列表,NSKVONotifying_Person
重写了监听属性的setter
方法- (void)printAllMethodsWithClass:(Class)class{ unsigned int count = 0; Method *list = class_copyMethodList(class, &count); for (int i = 0; i<count; i++) { Method method = list[I]; SEL sel = method_getName(method); IMP imp = class_getMethodImplementation(class, sel); NSLog(@"%@-%p",NSStringFromSelector(sel),imp); } free(list); }
![Person类与中间类.png](https://img.haomeiwen.com/i2438680/ea3c794377779555.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
-
在移除全部监听后,查看对象isa已被还原,但是中间类生成后不会被销毁
移除观察者后.png
KVO
监听赋值过程
-
在初始化
Person
对象时,断点调试监听赋值过程
watchpoint.pngwatchpoint set variable self->_person->_name
-
查看栈信息,在监听属性做赋值时会按下图顺序执行
-
_NSSetObjectValueAndNotify
-
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]:
-
-[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:
方法内部
NSKeyValueWillChange
[super setName:]
NSKeyValueDidChange
-
网友评论