was deallocated while key value observers were still registered with it.

今天在调用自己一个工具类的时候遇到了这个问题,大致的意思是注册了观察者,然后没有被注销掉,又开始重复使用了。也可以说 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 *)change
                      context:(void *)context
  • 4、假如遵循KVO规则的话,当被观察的对象的属性改变的时候,就会直接调用上面那个方法啦,同时移除掉对观察者的监听。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;

上述案例的完整代码

#import 

@interface Student : NSObject

@property (nonatomic, copy) NSString *name;

@end

#import "StudentObserver.h"

@implementation StudentObserver

- (void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)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:
 */

通过上面阐述的,我们大致了解了 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 *)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 *)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/

你可能感兴趣的:(was deallocated while key value observers were still registered with it.)