1、KVO介绍app
KVO(Key-Value Observing),键值监听。它提供一种机制:指定的被观察者的属性被改变后,KVO就会通知观察者,观察者能够作出响应。函数
KVO做用:利用KVO,很容易实现视图组件和数据模型的分离。当数据模型的属性值改变以后,做为监听者的视图组件就会被激发。这有利于业务逻辑和视图展现的解耦合。this
KVO使用步骤:(1)注册观察,添加观察者及属性;(2)实现回调方法;(3)移除观察。atom
(1)注册观察:spa
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context /* observer:观察者,也就是KVO通知的订阅者。订阅着必须实现observeValueForKeyPath:ofObject:change:context:方法 keyPath:描述将要观察的属性,相对于被观察者。 options:KVO的一些属性配置;有四个选项。 options所包括的内容: NSKeyValueObservingOptionNew:change字典包括改变后的值; NSKeyValueObservingOptionOld: change字典包括改变前的值; NSKeyValueObservingOptionInitial:注册后马上触发KVO通知; NSKeyValueObservingOptionPrior:值改变前是否也要通知(这个key决定了是否在改变前改变后通知两次). context: 上下文,这个会传递到订阅着的函数中,用来区分消息,因此应当是不一样的。 */
(2)实现回调方法:3d
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context; /* keyPath:被监听的keyPath , 用来区分不一样的KVO监听. object: 被观察修改后的对象(能够经过object得到修改后的值). change:保存信息改变的字典(可能有旧的值,新的值等) . context:上下文,用来区分不一样的KVO监听. */
(3)移除观察代理
- (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context /* 注意:不要忘记解除注册,不然会致使资源泄露 . */
2、KVO使用举例及注意事项code
//被观察者 StockData.m #import "StockData.h" @interface StockData() @property(nonatomic, strong)NSString *stockName; @property(nonatomic, strong)NSString *price; @end //观察者 SLVKVOController.m #import "SLVKVOController.h" #import "StockData.h" - (void)viewDidLoad { [super viewDidLoad]; [self.stockData setValue:@"searph" forKey:@"stockName"]; [self.stockData setValue:@"10.0" forKey:@"price"]; [self.stockData addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:SLVKVOContext]; } -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if(context == SLVKVOContext && object == self.stockData && [keyPath isEqualToString:@"price"]) { NSString * oldValue = [change objectForKey:NSKeyValueChangeOldKey]; NSString * newValue = [change objectForKey:NSKeyValueChangeNewKey]; self.myLabel.text = [NSString stringWithFormat:@"oldValue:%@ , newValue:%@",oldValue,newValue]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } -(void)dealloc { [self.stockData removeObserver:self forKeyPath:@"price" context:SLVKVOContext]; }
注意:orm
(1)在第二步回调observeValueForKeyPath:函数中,要用else进行判断调用super的对应函数。由于若当前函数没法处理对应的kvo,有可能super-class会有一些kvo的对应处理。server
(2)在第三步在dealloc函数中注销观察中,当对同一个keypath进行两次removeObserver时会致使程序crash,这种状况经常出如今父类有一个kvo,父类在dealloc中remove了一次,子类又remove了一次的状况下。能够利用context字段来标识出到底kvo是superClass注册的,仍是self注册的。咱们能够分别在父类以及本类中定义各自的context字符串,而后在dealloc中remove observer时指定移除的自身添加的observer。这样就能避免二次remove形成crash。
3、KVO常见crash及防御方案
KVO常见crash类型:
(1)不能对不存在的属性进行kvo观测,不然会报crash:uncaught exception 'NSUnknownKeyException', reason: '[<StockData 0x600000203d50> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key stockName.'
(2)订阅者必须实现 observeValueForKeyPath:ofObject:change:context:方法,不然crash。
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<SLVKVOController: 0x7f811372ff70>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
(3) 移除观察,超过addObserver的次数就会 crash:Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <SLVKVOController 0x7ff8e8703100> for the key path "price" from <StockData 0x60800003d000> because it is not registered as an observer.'
KVO crash解决方案:
方案1、
可让被观察对象持有一个KVO的delegate,全部和KVO相关的操做均经过delegate来进行管理,delegate经过创建一张map来维护KVO整个关系。
中间层delegate的代理工做:
(1)若是出现KVO重复添加观察者或者重复移除观察者(KVO注册观察者与移除观察者不匹配)的状况,delegate能够直接阻止这些非正常的操做。
(2)被观察者dealloc以前,能够经过delegate自动将与本身有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO致使的crash。
方案2、
咱们可让观察者在注册的过程当中,将注册信息一同记录下来,而后使用某种方法在对象dealloc时,在记录的信息里找到对应的观察者,注销观察。
此方案在宿主释放过程当中嵌入咱们本身的对象,使得宿主释放时顺带将咱们的对象一块儿释放掉,从而获取dealloc的时机点。采用构建一个释放通知对象,经过AssociatedObject方式链接到宿主对象,在宿主释放时进行回调,完成注销动做。
具体的原理和代码能够参照上一篇文章《[crash详解与防御] NSNotification crash》。