[crash详解与防护] KVO crash
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[crash详解与防护] KVO crash相关的知识,希望对你有一定的参考价值。
一、KVO介绍
KVO(Key-Value Observing),键值监听。它提供一种机制:指定的被观察者的属性被改变后,KVO就会通知观察者,观察者可以做出响应。
KVO作用:利用KVO,很容易实现视图组件和数据模型的分离。当数据模型的属性值改变之后,作为监听者的视图组件就会被激发。这有利于业务逻辑和视图展示的解耦合。
KVO使用步骤:(1)注册观察,添加观察者及属性;(2)实现回调方法;(3)移除观察。
(1)注册观察:
- (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)实现回调方法:
- (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 /* 注意:不要忘记解除注册,否则会导致资源泄露 . */
二、KVO使用举例及注意事项
//被观察者 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]; }
注意:
(1)在第二步回调observeValueForKeyPath:函数中,要用else进行判断调用super的对应函数。因为若当前函数无法处理对应的kvo,有可能super-class会有一些kvo的对应处理。
(2)在第三步在dealloc函数中注销观察中,当对同一个keypath进行两次removeObserver时会导致程序crash,这种情况常常出现在父类有一个kvo,父类在dealloc中remove了一次,子类又remove了一次的情况下。可以利用context字段来标识出到底kvo是superClass注册的,还是self注册的。我们可以分别在父类以及本类中定义各自的context字符串,然后在dealloc中remove observer时指定移除的自身添加的observer。这样就能避免二次remove造成crash。
三、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解决方案:
方案一、
可以让被观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张map来维护KVO整个关系。
中间层delegate的代理工作:
(1)如果出现KVO重复添加观察者或者重复移除观察者(KVO注册观察者与移除观察者不匹配)的情况,delegate可以直接阻止这些非正常的操作。
(2)被观察者dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash。
方案二、
我们可以让观察者在注册的过程中,将注册信息一同记录下来,然后使用某种方法在对象dealloc时,在记录的信息里找到对应的观察者,注销观察。
此方案在宿主释放过程中嵌入我们自己的对象,使得宿主释放时顺带将我们的对象一起释放掉,从而获取dealloc的时机点。采用构建一个释放通知对象,通过AssociatedObject方式连接到宿主对象,在宿主释放时进行回调,完成注销动作。
具体的原理和代码可以参照上一篇文章《[crash详解与防护] NSNotification crash》。
以上是关于[crash详解与防护] KVO crash的主要内容,如果未能解决你的问题,请参考以下文章
iOS中的crash防护unrecognized selector sent to instance
iOS 11. KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_