Important: In order to understand key-value observing, you must first understand key-value coding. -- 官方文档html
想要理解KVO,必须先理解KVC,由于键值观察是创建在键值编码 的基础上。观众老爷们能够参考笔者的上篇文章iOS进阶之路 (十三)KVC。ios
Key-value observing
is a mechanism that allows objects to be notified of changes to specified properties of other objects.git
KVO (Key-value observing)
是一个非正式协议,容许对象在其余对象的指定属性发生更改时获得通知。iOS开发者可使用KVO
来检测对象属性的变化、快速作出响应,这可以为咱们在开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。github
KVO’s primary benefit is that you don’t have to implement your own scheme to send notifications every time a property changes. Its well-defined infrastructure has framework-level support that makes it easy to adopt—typically you do not have to add any code to your project. In addition, the infrastructure is already full-featured, which makes it easy to support multiple observers for a single property, as well as dependent values.objective-c
KVO
的主要好处是,没必要在每次属性更改时都实现本身的方案来发送通知。这个过程大部分是内建的,自动的,透明的。这使得采用它很容易,一般您没必要向项目中添加任何代码。此外,KVO支持单个属性添加多个观察者以及依赖值。设计模式
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
复制代码
observer
: 注册 KVO 通知的对象。观察者必须实现 key-value observing
方法 - observeValueForKeyPath:ofObject:change:context:
。keyPath
: 被观察者的属性的 keypath
,相对于接受者,值不能是 nil。options
: 表明 NSKeyValueObservingOptions
的位掩码,它指定了观察通知中包含了什么context
:在 observeValueForKeyPath:ofObject:change:context:
传给 observer
参数的上下文传字符串作为 keypath 比直接使用属性更糟糕,由于任何错字或者拼写错误都不会被编译器察觉,最终致使不能正常工做。 一个聪明的解决方案是使用 NSStringFromSelector
和一个 @selector
字面值:数组
NSStringFromSelector(@selector(isFinished))
复制代码
由于 @selector
检查目标中的全部可用 selector
,这并不能阻止全部的错误,但它能够用来捕获大部分改变。安全
关于context,苹果官方文档也作了精彩的注释。bash
The context pointer in the addObserver:forKeyPath:options:context: message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.网络
context
中包含着将在相应的更改通知中传递回观察员的任意数据。您能够指定NULL并彻底依赖于keyPath
肯定更改通知的来源。可是这种方法可能出现问题:尤为是处理那些继承自同一个父类的子类,而且这些子类有相同的 keypath
。
A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
一种更安全、更可扩展的方法是:使用context
来确保接收到的通知是发送给观察者的,而不是发送给超类。 如何设置一个好的 content
呢?苹果官方文档也给出了推荐的方法。
// a Person instance registers itself as an observer for an Account instance’s
// 大致意思就是:一个静态变量存着它本身的指针。这意味着它本身什么也没有。
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
复制代码
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
复制代码
苹果官方文档推荐在 init
或 viewDidLoad
add观察者,在 dealloc
里移除观察者, 保证 add
和 remove
是成对出现的。
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
复制代码
调用 –removeObserver:forKeyPath:context:
时, 当这个对象没有被注册为观察者(由于它已经解注册了或者开始没有注册),会抛出一个异常。有意思的是,没有一个内建的方式来检查对象是否注册。 苹果官方文档推荐使用: @try
/ @catch
移除观察者。
- (void)dealloc {
@try {
// 3. unsubscribe
[_account removeObserver:self
forKeyPath:NSStringFromSelector(@selector(contentSize))
context:ContentSizeContext];
}
@catch (NSException *exception) {
}
}
复制代码
// key 方法
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
// keyPath方法:
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
复制代码
例子:
To use KVO, first you must ensure that the observed object, the Account in this case, is KVO compliant. Typically, if your objects inherit from NSObject and you create properties in the usual way, your objects and their properties will automatically be KVO Compliant. It is also possible to implement compliance manually. KVO Compliance describes the difference between automatic and manual key-value observing, and how to implement both.
要使用KVO
,首先必须确保被观察的对象符合KVO
。一般,若是您的对象继承自NSObject
,而且您以一般的方式建立属性,那么您的对象及其属性将自动与KVO
兼容。固然,也能够手动实现听从性。KVO Compliance
讲述述了自动和手动键值观察之间的区别,以及如何实现二者。
能够经过复写 automaticallyNotifiesObserversForKey:
的返回值,选择自动退出 KVO。同时该类方法还能完成控制特定属性的目的。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
复制代码
// 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];
复制代码
手动 KVO 能够帮助咱们将多个属性值的更改合并成一个,这样在回调的时候就有一次了,同时也能最大程度地减小处于应用程序特定缘由而致使的通知发生。
willChangeValueForKey
,在更改值以后调用didChangeValueForKey
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
复制代码
- (void)setBalance:(double)theBalance {
if (theBalance != _balance) {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
}
复制代码
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
复制代码
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"];
}
复制代码
有一些属性的值取决于一个或者多个其余对象的属性值,一旦某个被依赖的属性值变了,依赖它的属性的变化也须要被通知。
要自动触发 To-One
关系,有两种方法:
keyPathsForValuesAffectingValueForKey:
方法keyPathsForValuesAffecting<Key>
的方法。举个例子: 一我的的全名 fullName 是由 firstName 和 lastName 组成的,一个观察 fullName 的程序在 firstName 或者 lastName 变化时也应该接收到通知。
- (NSString *)fullName
{
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
复制代码
keyPathsForValuesAffectingValueForKey:
方法, 来代表 fullname 属性是依赖于 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;
}
复制代码
至关于在影响 fullName 值的 keypath 中新加了两个key:lastName 和 firstName,很容易理解。
值得注意的是:须要先对父类发送 keyPathsForValuesAffectingValueForKey
消息,以避免干扰父类中对此方法的重写
+ (NSSet *)keyPathsForValuesAffectingFullName
{
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
复制代码
若是在分类中,使用 keyPathsForValuesAffectingFullName
更合理,由于分类中是不容许重载方法的,因此 keyPathsForValuesAffectingValueForKey
方法确定是不能在分类中使用的。
keyPathsForValuesAffectingValueForKey:
方法不支持包含 to-many
关系的 keypath
。
好比,有一个 Department(部门)
类,它有一个针对 Employee(雇员)
类的 to-many 关系,Employee
类有 salary(薪资)
属性。你但愿 Department
类有一个 totalSalary
属性来计算全部员工的薪水,也就是 Department的totalSalary
依赖于全部 Employee的salary
属性。你不能经过实现 keyPathsForValuesAffectingTotalSalary
方法并返回 employees.salary
。
你能够用KVO将parent
(好比Department)做为全部children
(好比Employee)相关属性的观察者。你必须在把child添加或删除到parent时也把parent做为child的观察者添加或删除。在observeValueForKeyPath:ofObject:change:context:
方法中咱们能够针对被依赖项的变动来更新依赖项的值:
- (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:@"employees.@sum.salary"]];
}
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
if (totalSalary != newTotalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = newTotalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary {
return _totalSalary;
}
复制代码
将 Department 实例对象注册为观察者,而后观察对象为 totalSalary 属性,可是在通知回调中会手动调用 totalSalary 属性的 setter 方法,而且传入值是经过 KVC 的集合运算符的方式取出 employees 属性所对应的集合中全部 sum 值之和。而后在 totalSalary 属性的 setter 方法中,会相应的调用 willChangeValueForKey: 和 didChangeValueForKey: 方法。
先看看官方文档的解释
Automatic key-value observing is implemented using a technique called isa-swizzling.
isa-swizzling
。The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
isa
指针所指向的内容发生了变化,指向了一个中间类而不是真正的类。这也致使 isa
指针并不必定是指向实例所属的真正的类。You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
isa
指针来肯定类成员身份。相反,你应该使用 class
方法来肯定对象实例所属的类。根据官网文档,咱们初步判断,在 KVO 底层会有一个中间类生成,中间类会让对象的 isa 指针发生变化。测试一下:
self.person
的isa 指向 AKPerson类
self.person
的isa 指向 NSKVONotifying_AKPerson类
结论1:添加观察者后,系统动态生成了中间类
NSKVONotifying_AKPerson
,实例对象self.person
的isa由指向原始AKPerson类
, 修改成指向中间类NSKVONotifying_AKPerson
。
那么这个中间类和原来的类是什么关系呢? 遍历下二者的类以及子类,测试一下
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls {
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 建立一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取全部已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
复制代码
结论2:添加观察者以后,中间类 NSKVONotifying_AKPerson 是原始类
AKPerson
的子类.
KVO关注的是属性值的变化,而 属性 = 成员变量 + getter + setter
,显然只有 setter 和成员变量赋值两种方式能够改变属性值。
咱们作下测试:AKPerson 添加成员变量name
和 属性nickName
, 添加观察者后同时修改二者的值:
@interface AKPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
复制代码
结论3: 动态子类观察的是setter方法
咱们能够遍历原始类和中间类的方法列表, 观察下中间类重写了哪些方法?
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@"***** %@ *****", cls);
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
复制代码
AKPerson类
中的方法没有改变(imp实现地址没有变化)NSKVONotifying_AKPerson中间类
重写了 父类AKPerson
的 dealloc方法
NSKVONotifying_AKPerson中间类
重写了 基类NSObject
的 class方法
和 _isKVOA方法
_isKVOA
方法:作个标识class方法
仍然是返回的是原始类,目的就是隐藏中间类的存在,让调用者调用 class方法
结果先后一致NSKVONotifying_AKPerson中间类
指会 AKPerson类
经过observationInfo
命令,能够在 lldb 里查看一个被观察对象的全部观察信息。
KVO 是 Cocoa 为咱们提供的一种模式,用于订阅其余对象属性的更改。 KVO 很强大,没错。知道它内部实现,或许能帮助更好地使用它,或在它出错时更方便调试。但官方 KVO 提供的 API 并很差用。甚至咱们的平常开发中,除非万不得已,不多去使用官方 KVO 的 API。
在 Soroush Khanlou 的 Key-Value Observing Done Right 文章中,KVO 被批判的体无完肤。好比:
KVO all comes through one method
: 只能经过重写 -observeValueForKeyPath:ofObject:change:context: 方法来得到通知,想要提供自定义的 selector ,想要传一个 block ,没门!KVO is string-ly typed
: 传递的 keyPath 是字符串类型的,任何错字或者拼写错误都不会被编译器察觉,最终致使不能正常工做。KVO requires you to handle superclasses yourself
: KVO要求咱们本身处理超类的问题。尤为是处理那些继承自同一个父类的子类,而且这些子类有相同的 keypath。KVO can crash when deregistering
:KVO 须要手动移除,移除的时候还可能引发崩溃,并且 KVO 没有一个内建的方式来检查对象是否注册或销毁。例如:一个超类也观察同一个对象上的同一个参数,会发生什么状况?它会被移除两次,第二次会致使崩溃。若是想完美的解决这些问题,建议阅读下强大的自定义KVO框架 facebookarchive/KVOController
在 Objective-C
和 Cocoa
中,有许多事件之间进行通讯的方式,而且每一个都有不一样程度的形式和耦合
NSNotification
:Key-Value Observing
:Delegate
Callbacks
最后附上KVO的流程图: