在使用KVO的时候添加观察者我们是这样做的:
// 监听btn 的selected属性改变[self addObserver:self forKeyPath:@"selected" options:NSKeyValueObservingOptionNew context:nil];
需要特别注意的是:在dellaoc 的时候我们需要将监听的observer移除:
- (void)dealloc { [self removeObserver:self forKeyPath:@"selected"];}
上面是在正常不过的操作了,但是我们在实际的项目可能会遇到
1.重复添加observer这种情况属性值的改变会回调:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context {}
这个方法多次。
2.某个属性根本没有添加observer,但是却在dealloc中移除了这个属性的观察者,或者是重复移除。这种情况会造成app 的闪退。
解决的方案是:创建一个NSObject的分类,运用iOS的运行时特性在运行时交换 addObserver 和 removeObserver的方法实现 ,通过读取 self.observationInfo 内容的值判断有误监听的属性值来进行添加和删除的操作 。self.observationInfo 是一个 void * 类型,下面是打印 self.observationInfo 私有属性 _observances 的值
(lldb) po [info valueForKey:@"_observances"]__NSArrayI 0x604000032f00(Context: 0x0, Property: 0x604000051af0>, Context: 0x0, Property: 0x604000051c40>
发现里面存储的使我们已经添加的observers 集合。 每个NSKeyValueObservcance对面里面又存储了Observer的相关信息。 如下图:
通过获取 _keyPath的值,可以判断是不是已经添加或者存在该 observer,根据该值决定是否在我们自己的addObserver和removeObser方法中添加或删除observser 。
具体的实现如下:
.h文件
@interface NSObject (SafeKVO)/*! @method @abstract 移除所有观察的keypath */- (void)removeAllObserverdKeyPath;@end
.m文件
#import "NSObject+SafeKVO.h"#import@implementation NSObject (SafeKVO)+ (void)load { Method originAddM = class_getInstanceMethod([self class], @selector(addObserver:forKeyPath:options:context:)); Method swizzAddM = class_getInstanceMethod([self class], @selector(swizz_addObserver:forKeyPath:options:context:)); Method originRemoveM = class_getInstanceMethod([self class], @selector(removeObserver:forKeyPath:context:)); Method swizzRemoveM = class_getInstanceMethod([self class], @selector(swizz_removeObserver:forKeyPath:context:)); IMP originAddMIMP = class_getMethodImplementation([self class], @selector(addObserver:forKeyPath:options:context:)); IMP originRemoveIMP = class_getMethodImplementation([self class], @selector(removeObserver:name:object:)); BOOL hasAddM = class_addMethod([self class], @selector(addObserver:forKeyPath:options:context:), originAddMIMP, method_getTypeEncoding(originAddM)); BOOL hasRemoveM = class_addMethod([self class], @selector(removeObserver:forKeyPath:context:), originRemoveIMP, method_getTypeEncoding(originRemoveM)); // excute once static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ if (!hasAddM) { method_exchangeImplementations(originAddM, swizzAddM); } if (!hasRemoveM) { method_exchangeImplementations(originRemoveM, swizzRemoveM); } });}- (void)swizz_addObserver:(nonnull NSObject *)observer forKeyPath:(nonnull NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context { if (![self hasKey:keyPath]) { // 调用系统的添加observer 方法 [self swizz_addObserver:observer forKeyPath:keyPath options:options context:context]; }}- (void)swizz_removeObserver:(nonnull NSObject *)observer forKeyPath:(nonnull NSString *)keyPath context:(nullable void *)context { if ([self hasKey:keyPath]) { [self swizz_removeObserver:observer forKeyPath:keyPath context:context]; }}- (void)removeAllObserverdKeyPath { id info = self.observationInfo; NSArray *arr = [info valueForKeyPath:@"_observances._property._keyPath"]; for (NSString *keyPath in arr) { // TODO context 需要考虑值不为nil的时候 [self removeObserver:self forKeyPath:keyPath context:nil]; }}- (BOOL)hasKey:(NSString *)kvoKey { BOOL hasKey = NO; id info = self.observationInfo; NSArray *arr = [info valueForKeyPath:@"_observances._property._keyPath"]; for (id keypath in arr) { // 存在kvo 的key if ([keypath isEqualToString:kvoKey]) { hasKey = YES; break; } } return hasKey;}@end
把上面两个文件拖入到项目中就可以解决kvo重复添加或者删除observer的问题了 。
以上内容为个人总结,如果问题或疑问请留言联系。