此文实际成于 2015/08/11
KVO 与实例变量
如何正确的对实例变量进行 KVO 编程?
下面是一个简单的带有一个实例变量的类。
@interface KVOObject : NSObject{
@public
NSString *_foo;
}
@end
@implementation KVOObject @end
然后是一个测试类,进行 KVO
@interface KVOObjectDemo : NSObject
@property (nonatomic,strong) KVOObject *kvoObject;
@end
@interface KVOObjectDemo : NSObject
@property (nonatomic,strong) KVOObject *kvoObject;
@end
NSString * const fooKeyPath = @"_foo";
@implementation KVOObjectDemo
- (instancetype)init
{
self = [super init];
if (self) {
_kvoObject = [KVOObject new];
[_kvoObject addObserver:self forKeyPath:fooKeyPath options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%s keyPath:%@",__FUNCTION__,keyPath);
if ([fooKeyPath isEqualToString:keyPath]) {
NSLog(@"foo value changed to \"%@\" instance value:\"%@\"",change[NSKeyValueChangeNewKey],_kvoObject->_foo);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
printf("dealloc");
[_kvoObject removeObserver:self forKeyPath:fooKeyPath];
}
@end
测试代码:
KVOObjectDemo *kvodemo = [KVOObjectDemo new];
kvodemo.kvoObject->_foo = @"good";
[kvodemo.kvoObject setValue:@"really cool" forKey:fooKeyPath];
- 首先是设置
fooKeyPath
为_foo
运行输出结果如下 :
2015-08-11 08:41:06.684 ObjcDemo[34376:17203392] -[KVOObjectDemo observeValueForKeyPath:ofObject:change:context:] keyPath:_foo
2015-08-11 08:41:06.684 ObjcDemo[34376:17203392] foo value changed to "really cool" instance value:"really cool"
dealloc
结果显示 KVO 编程成功。
- 再设置
fooKeyPath
是foo
运行输出结果如下 :
2015-08-11 08:43:16.816 ObjcDemo[34426:17204277] -[KVOObjectDemo observeValueForKeyPath:ofObject:change:context:] keyPath:foo
2015-08-11 08:43:16.817 ObjcDemo[34426:17204277] foo value changed to "really cool" instance value:"really cool"
dealloc
结果也是 OK 的。
查看文档发现 setValue:forKey:
帮我们做了大量的事情。
setValue:foKey:
的默认搜索顺序
- 搜索接收类 与
set<Key>:
匹配的setter
- 如果没有找到
setter
并且类方法accessInstanceVariableDirectly
返回YES
(此类方法,默认返回 YES)
搜索接收类中实例变量的名字与_<key>
,_is<Key>
,<key>
,is<Key>
匹配的实例变量。
如果匹配的
setter
或者实例变量找到了,将用于赋值。如果有必要的话,
值将以在Representing Non-Object Values 讨论的方式从对象中抽取出来。如果都没有搜索到,将调用接收者的
setValue:forUndefinedKey:
, 此方法的默认实现是抛出NSUndefinedKeyException
,你可以重写些方法。如果有对应的
setter
,并且如果参数类型不是一个对象指针,但是值为nil
,此时将调用。-setNilValueForKey:
其默认实现是抛出NSInvalidArgumentException
.但你可以重写此方法。如果是其他类型,在调用相应方法时NSNumber/NSValue
转换会提前进行。
参考: Accessor Search Implementation Details
KVO 的几个选项
public struct NSKeyValueObservingOptions:OptionSetType{
public static var New: NSKeyValueObservingOptions { get }
public static var Old: NSKeyValueObservingOptions { get }
@available(iOS 2.0, *)
public static var Initial: NSKeyValueObservingOptions { get }
@available(iOS 2.0, *)
public static var Prior: NSKeyValueObservingOptions { get }
}
在 observe 回调方法:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
}
有这么一个参数: change: [String : AnyObject]?
他是一个字典。我们知道 我们使用 KVO 就是想当某个值变化的时候通知我们。告诉我们值的变化。
如果 KVO 选项中带有 .New
表示变更列表中需要包含新的值,
.Old
表示变更列表中带上新的值。
.Initial
这个选项是新加的, 这是用来处理这种场景的,就是当我们添加此 observer时是否应该马上
调用的observeValueForKeyPath:....
这个函数。来告诉此方法要感兴趣的的属性现在的值。
一些错误
注册了某一个KeyPath 但是没有处理会报错
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
2015-11-16 12:13:17.554 BXLoadMoreControl_Example[9020:15633890] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<BXLoadMoreControl.BXLoadMoreControlHelper: 0x7fef426438c0>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: contentOffset
Observed object: <UITableView: 0x7fef4281c000; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x7fef42450680>; layer = <CALayer: 0x7fef42447980>; contentOffset: {0, 0}; contentSize: {600, 0}>
Observer 没有移除,但是对象已经被释放
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x15e44460 of class AVAudioPlayer was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x15e42630> (
<NSKeyValueObservance 0x15ea17b0: Observer: 0x15d71a00, Key path: currentTime, Options: <New: YES, Old: NO, Prior: NO> Context: 0x15d71ba8, Property: 0x15ea1770>