今天在调用自己一个工具类的时候遇到了这个问题,大致的意思是注册了观察者,然后没有被注销掉,又开始重复使用了。也可以说 KVO 使用不当造成的,所以在此先来了解下 KVO。
通俗的说,KVO 就是我们是用来设值或取值的协议(NSKeyValueCoding
)。通过对变量和函数名进行规范达到方便设置类成员值的目的。它是Cocoa的一个重要机制,它有点类似于Notification,但是,它提供了观察某一属性变化的方法,而Notification需要一个发送notification的对象,这样KVO就比Notification极大的简化了代码。
KVO 如何工作的呢?
- 1、两个对象,其中一个对象的属性发生改变的时候,另一个对象可以监测到。
// 对象(被观察者)
@interface Student : NSObject
// 观察者
@interface StudentObserver : NSObject
- 2、两个对象间通过 ”addObserver:forKeyPath:options:context:“建立起连接
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
- 3、为了能够响应消息,作为观察者的对象必须实现下面这个方法。这个方法实现如何响应变化的消息。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
- 4、假如遵循KVO规则的话,当被观察的对象的属性改变的时候,就会直接调用上面那个方法啦,同时移除掉对观察者的监听。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
上述案例的完整代码
#import <Foundation/Foundation.h>
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end
#import "StudentObserver.h"
@implementation StudentObserver
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
NSLog(@"old = %@",[change objectForKey:NSKeyValueChangeOldKey]);
NSLog(@"new = %@",[change objectForKey:NSKeyValueChangeNewKey]);
NSLog(@"context:%@",context);
}
@end
Student *student = [[Student alloc] init];
student.name = @"yang";
StudentObserver *studentObserver = [[StudentObserver alloc] init];
[student addObserver:studentObserver
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
context:(void*)self];
student.name = @"liu";
[student removeObserver:studentObserver forKeyPath:@"name"];
/**
* input
old = yang
new = liu
context:<ViewController: 0x7f98c84a0720>
*/
通过上面阐述的,我们大致了解了 KVO可以很好的观察某一属性变化的方法。
详细了解下下面几个方法
注册观察者方法
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
* anObserver:观察者对象,这个对象必须实现observeValueForKeyPath:ofObject:change:context:方法,以响应属性的修改通知。
* keyPath:被监听的属性。这个值不能为nil。
* options:监听选项,这个值可以是NSKeyValueObservingOptions选项的组合。关于监听选项,我们会在下面介绍。
* context:任意的额外数据,我们可以将这些数据作为上下文数据,它会传递给观察者对象的observeValueForKeyPath:ofObject:change:context:方法。这个参数的意义在于用于区分同一对象监听同一属性(从属于同一对象)的多个不同的监听
// options
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
// 提供属性的新值
NSKeyValueObservingOptionNew = 0x01,
// 提供属性的旧值
NSKeyValueObservingOptionOld = 0x02,
// 如果指定,则在添加观察者的时候立即发送一个通知给观察者,
// 并且是在注册观察者方法返回之前
NSKeyValueObservingOptionInitial NS_ENUM_AVAILABLE(10_5, 2_0) = 0x04,
// 如果指定,则在每次修改属性时,会在修改通知被发送之前预先发送一条通知给观察者
// 这与-willChangeValueForKey:被触发的时间是相对应的。
// 这样,在每次修改属性时,实际上是会发送两条通知。
NSKeyValueObservingOptionPrior NS_ENUM_AVAILABLE(10_5, 2_0) = 0x08
};
处理属性观察者方法
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context
keyPath:即被观察的属性,与参数object相关。
object:keyPath所属的对象。
change:这是一个字典,它包含了属性被修改的一些信息。这个字典中包含的值会根据我们在添加观察者时设置的options参数的不同而有所不同。
context:这个值即是添加观察者时提供的上下文信息。
// change key
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);
再通过一个我们可能用到的例子加深理解,监听tableView的 contentOffset 中的偏移量。
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView addObserver: self
forKeyPath: @"contentOffset"
options: NSKeyValueObservingOptionNew
context: nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
CGFloat offset = self.tableView.contentOffset.y;
NSLog(@"self.table.contentOffset === %f",offset);
}
- (void)dealloc {
[self.tableView removeObserver:self
forKeyPath:@"contentOffset"
context:nil];
}
像dealloc 中最好是加一个 @try异常捕获的写法,更安全。
- (void)dealloc {
@try {
[self.item removeObserver:self forKeyPath:@"content"];
}
@catch (NSException *exception) {
NSLog(@"Exception: %@", exception);
}
@finally {
// Added to show finally works as well
}
}
总得说来,KVO 很强大,也有很多坑,更详细的了解可以仔细看看Foundation: NSKeyValueObserving(KVO),至此笔记先到这。
回过头了,上述那个问题,还是没有说怎么解决,首先要说明的是使用 KVO 需要小心,需要养成好习惯。上述那问题就是提示我们注册了观察者不要忘记了注销,可以尝试在 dealloc 中注销下试试。
备注
http://zhangbuhuai.com/2015/04/29/understanding-KVO/
http://southpeak.github.io/blog/2015/04/23/cocoa-foundation-nskeyvalueobserving/