[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防护KVC造成的crash

.cxx_destruct crash

iOS Crash防护你看这个就够了(下)

iOS 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_