iOS开发 - 黑科技防止多次添加删除KVO出现的问题

------更新------:
之前没有判断observer是否一致,有个别情况会无法处理,所以更新添加了observer判断

一、使用场景

有时候我们会忘记添加多次KVO监听或者,不小心删除如果KVO监听,如果添加多次KVO监听这个时候我们就会接受到多次监听。
如果删除多次kvo程序就会造成catch,如下图


这时候我们就可以想一些方案来防止这种情况的发生。


二、使用技术

核心 : 利用runtime实现方法交换,进行拦截add和remove进行操作。

  1. 方案一 :利用 @try @catch
  2. 方案二 :利用 模型数组 进行存储记录
  3. 方案二 :利用 observationInfo 里私有属性

(1) 方案一(只能针对删除多次KVO的情况下)
利用 @try @catc

不得不说这种方法真是很Low,但是很简单就可以实现。
这种方法只能针对多次删除KVO的处理,原理就是try catch可以捕获异常,不让程序catch。这样就实现了防止多次删除KVO。

@try {
        [self.btn removeObserver:self forKeyPath:@"kkl"];
    } 
@catch (NSException *exception) {
        NSLog(@"多次删除了");
}

普通情况下,使用这种方法就需要每次removeObserver的时候,就加上去一个@try @catch
有个简单的方法:给NSObject 增加一个分类,然后利用Run time 交换系统的 removeObserver方法,在里面添加 @try @catch。

runtime 就不多说了,大家自己自己查下相关资料有很多。
下面就直接上实现代码了:

NSObject+DSKVO.m

#import "NSObject+DSKVO.h"
#import <objc/runtime.h>
@implementation NSObject (DSKVO)

+ (void)load
{
    [self switchMethod];
}

// 交换后的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    @try {
        [self removeDasen:observer forKeyPath:keyPath];
    } @catch (NSException *exception) {}
}

+ (void)switchMethod
{
    SEL removeSel = @selector(removeObserver:forKeyPath:);
    SEL myRemoveSel = @selector(removeDasen:forKeyPath:);

    Method systemRemoveMethod = class_getClassMethod([self class],removeSel);
    Method DasenRemoveMethod = class_getClassMethod([self class], myRemoveSel);

    method_exchangeImplementations(systemRemoveMethod, DasenRemoveMethod);
}

@end

(2) 方案二
利用 模型数组 进行存储记录

第一步 利用交换方法,拦截到需要的东西
1,是在监听哪个对象。
2,是在监听的keyPath是什么。

第二步 存储思路
1,我们需要一个模型用来存储
哪个对象执行了addObserver、监听的KeyPath是什么。
2,我们需要一个数组来存储这个模型。

第三步 进行存储
1,利用runtime 拦截到对象和keyPath,创建模型然后进行赋值模型相应的属性。
2,然后存储进数组中去。

第三步 存储之前的检索处理
1,在存储之前,为了防止多次addObserver相同的属性,这个时候我们就可以,遍历数组,取出每个一个模型,然后取出模型中的对象,首先判断对象是否一致,然后判断keypath是否一致2,对于添加KVO监听:如果不一致那么就执行利用交换后方法执行addObserver方法。

3,对于删除KVO监听: 如果一致那么我们就执行删除监听,否则不执行。

4,上代码了:
NSObject+DSKVO.m

#import "NSObject+DSKVO.h"
#import "DSObserver.h"
#import "ObserverData.h"
#import <objc/runtime.h>
@implementation NSObject (DSKVO)

+ (void)load
{
    [self switchMethod];
}

// 交换后的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    NSMutableArray *Observers = [DSObserver sharedDSObserver];
    ObserverData *userPathData = [self observerKeyPath:keyPath];
    // 如果有该key值那么进行删除
    if (userPathData) {
        [Observers removeObject:userPathData];
        [self removeDasen:observer forKeyPath:keyPath];
    }
 return;
}

// 交换后的方法
- (void)addDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    ObserverData *userPathData= [[ObserverData alloc]initWithObjc:self key:keyPath];
    NSMutableArray *Observers = [DSObserver sharedDSObserver];

    // 如果没有注册,那么才进行注册
    if (![self observerKeyPath:keyPath]) {
        [Observers addObject:userPathData];
        [self addDasen:observer forKeyPath:keyPath options:options context:context];
    }

}

// 进行检索,判断是否已经存储了该Key值
- (ObserverData *)observerKeyPath:(NSString *)keyPath
{
    NSMutableArray *Observers = [DSObserver sharedDSObserver];
    for (ObserverData *data in Observers) {
        if ([data.objc isEqual:self] && [data.keyPath isEqualToString:keyPath]) {
                return data;
        }
    }
    return nil;
}

+ (void)switchMethod
{
    SEL removeSel = @selector(removeObserver:forKeyPath:);
    SEL myRemoveSel = @selector(removeDasen:forKeyPath:);
    SEL addSel = @selector(addObserver:forKeyPath:options:context:);
    SEL myaddSel = @selector(addDasen:forKeyPath:options:context:);

    Method systemRemoveMethod = class_getClassMethod([self class],removeSel);
    Method DasenRemoveMethod = class_getClassMethod([self class], myRemoveSel);
    Method systemAddMethod = class_getClassMethod([self class],addSel);
    Method DasenAddMethod = class_getClassMethod([self class], myaddSel);

    method_exchangeImplementations(systemRemoveMethod, DasenRemoveMethod);
    method_exchangeImplementations(systemAddMethod, DasenAddMethod);
}

ObserverData 模型类文件有两个属性

@property (nonatomic, strong)id objc;
@property (nonatomic, copy)  NSString *keyPath;

DSObserver 类是一个单例数组

@implementation DSObserver
+ (instancetype)sharedDSObserver
{
    static id objc;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        objc = [NSMutableArray array];
    });
    return objc;
}

@end

(3) 方案三
利用 observationInfo 里私有属性

第一步 简单介绍下observationInfo属性
1,只要是继承与NSObject的对象都有observationInfo属性.
2,observationInfo是系统通过分类给NSObject增加的属性。
3,分类文件是NSKeyValueObserving.h这个文件
4,这个属性中存储有属性的监听者,通知者,还有监听的keyPath,等等KVO相关的属性。
5,observationInfo是一个void指针,指向一个包含所有观察者的一个标识信息对象,信息包含了每个监听的观察者,注册时设定的选项等。

@property (nullable) void *observationInfo;

6,observationInfo结构 (箭头所指是我们等下需要用到的地方)


第二步 实现方案思路
1,通过私有属性直接拿到当前对象所监听的keyPath,和observer

2,判断keyPath是否有无,和observer是否对应一直,来实现防止多次重复添加和删除KVO监听。

3,通过Dump Foundation.framework 的头文件,和直接xcode查看observationInfo的结构,发现有一个数组用来存储NSKeyValueObservance对象,经过测试和调试,发现这个数组存储的需要监听的对象中,监听了几个属性,如果监听两个,数组中就是2个对象。
比如这是监听两个属性状态下的数组


4,NSKeyValueObservance属性简单说明
_observer属性:里面放的是监听属性的通知这,也就是当属性改变的时候让哪个对象执行observeValueForKeyPath的对象。
_property 里面的NSKeyValueProperty NSKeyValueProperty存储的有keyPath,其他属性我们用不到,暂时就不说了。


5,拿出keyPath
这时候思路就有了,首先拿出_observances数组,然后遍历拿出里面_property对象里面的NSKeyValueProperty下的一个keyPath,然后进行判断需要删除或添加的keyPath是否一致,然后再次判断传递过来的observer和监听的是否一致,然后分别进行处理就行了。


6,上代码

#import "NSObject+DSKVO.h"
#import <objc/runtime.h>
@implementation NSObject (DSKVO)

+ (void)load
{
    [self switchMethod];
}

// 交换后的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
 if ([self observerKeyPath:keyPath observer:observer]) {
        [self removeDasen:observer forKeyPath:keyPath];
    }
}

// 交换后的方法
- (void)addDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
  if (![self observerKeyPath:keyPath observer:observer]) {
        [self addDasen:observer forKeyPath:keyPath options:options context:context];
    }
}

// 进行检索获取Key
- (BOOL)observerKeyPath:(NSString *)key observer:(id )observer
{
    id info = self.observationInfo;
    NSArray *array = [info valueForKey:@"_observances"];
    for (id objc in array) {
        id Properties = [objc valueForKeyPath:@"_property"];
        id newObserver = [objc valueForKeyPath:@"_observer"];
        
        NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];
        if ([key isEqualToString:keyPath] && [newObserver isEqual:observer]) {
            return YES;
        }
    }
    return NO;
}
+ (void)switchMethod
{
    SEL removeSel = @selector(removeObserver:forKeyPath:);
    SEL myRemoveSel = @selector(removeDasen:forKeyPath:);
    SEL addSel = @selector(addObserver:forKeyPath:options:context:);
    SEL myaddSel = @selector(addDasen:forKeyPath:options:context:);

    Method systemRemoveMethod = class_getClassMethod([self class],removeSel);
    Method DasenRemoveMethod = class_getClassMethod([self class], myRemoveSel);
    Method systemAddMethod = class_getClassMethod([self class],addSel);
    Method DasenAddMethod = class_getClassMethod([self class], myaddSel);

    method_exchangeImplementations(systemRemoveMethod, DasenRemoveMethod);
    method_exchangeImplementations(systemAddMethod, DasenAddMethod);
}

参考文章:http://www.bkjia.com/IOSjc/993206.html
参考人员:tyh
github地址:https://github.com/DaSens/DSKVO

感谢各位阅读,有什么补充的希望大家提出来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容