美文网首页iOS移动开发社区iOS进阶指南iOS Developer
iOS拦截系统KVO监听,防止多次删除和添加

iOS拦截系统KVO监听,防止多次删除和添加

作者: 那是一阵清风_徐来 | 来源:发表于2016-09-09 11:15 被阅读1231次

    最近项目中处理kvo 的时候,遇到一个问题:当我操作的时候,会发现kvo 释放的时候,会崩溃, 崩溃日志如下:

    /*Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observerfor the key path "kvoState" frombecause it is not registered as an observer.'*/

    经过反复研究,发现了错误的原因,并且找到解决错误的办法下面我将介绍一下我的思路:(慢慢来 跟着我的思路走)

    1.我在AppDelegate里面添加一个属性@property(nonatomic,copy)NSString *kvoState;/*测试kvo设置的一个字段 */

    2.我在我创建的一个ViewController(SecondViewController)里面去监听这个属性

    - (void)monitorNet { 

            AppDelegate *appDelegate = (AppDelegate *)[UIApplicationsharedApplication].delegate;    // kvo监听属性值的改变   

           [appDelegate addObserver:selfforKeyPath:@"kvoState"options:NSKeyValueObservingOptionNewcontext:nil];}

    /** 

    *  kvo

     */

    - (void)observeValueForKeyPath:(NSString *)keyPath        // 监听的属性名称                 

                                        ofObject:(id)object                  // 被监听的对象                   

                                          change:(NSDictionary *)change      // 属性的值             

                                          context:(void *)context            // 添加监听时传来的值   {   

            AppDelegate *appDelegate = (AppDelegate *)[UIApplicationsharedApplication].delegate;   

            if ([keyPath isEqualToString:@"kvoState"]) {                

                  NSNumber *number = [change objectForKey:@"new"];   

                  NSInteger item = [number integerValue];     

                  NSLog(@"%@====",appDelegate.kvoState);           

                  NSLog(@"%@----",number);     

                  if ([object isKindOfClass:[AppDelegateclass]] ) {                

                  } 

             }  

      }

    然后我再去释放 复写系统 dealloc 这个方法

    -(void)dealloc {   

           AppDelegate *appDelegate = (AppDelegate *)[UIApplicationsharedApplication].delegate;   

            [appDelegate removeObserver:selfforKeyPath:@"kvoState"];

    }

    3.在第二步之后,我点击一个button ,push 到 另外一个ViewController(TestViewController)里面,然后在TestViewController里面,点击button ,在这个button 的点击事件里面去执行下面的代码:(特地演示错误)

    -(void)buttonAction{  

            SecondViewController *secondVC = [[SecondViewControlleralloc]init];/*执行此行代码回报上述的错误*/             

            [self.navigationControllerpopViewControllerAnimated:YES];

    }

     当这个方法执行完之后,就会出现前面所展示的错误

    /*Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observerfor the key path "kvoState" frombecause it is not registered as an observer.'*/

    为什么会出现这种错误呢????其实出现这种错误也很简单的:首先在buttonAction 这个方法内,secondVC 他是一个局部变量,现在是ARC 管理,当这个方法执行完成以后,会销毁 secondVC 这个对象,那么,很自然的就会调用 SecondViewController 里面的 dealloc 这个方法

    -(void)dealloc{

        AppDelegate *appDelegate = (AppDelegate *)[UIApplicationsharedApplication].delegate; 

        [appDelegate removeObserver:selfforKeyPath:@"kvoState"];

    }

    appDelegate 的属性kvoState 会被remove,但是的这个时候,it is not registered as an observer所有,就会重新上述的崩溃现象说了这么多,大家能理解这个崩溃的原因了吗?(PS:不懂的话也请继续了解下面的内容)

    总之就是:有时候我们会忘记添加多次KVO监听或者,不小心删除如果KVO监听,如果添加多次KVO监听这个时候我们就会接受到多次监听。如果删除多次kvo程序就会造成catch既然问题的出现,那么,肯定会伴随着事务的解决

    下面我讲给大家讲解几个解决的方法(百度查资料的,亲自验证,安全可靠),

    方案有三种:/**    *  那么iOS开发-黑科技防止多次添加删除KVO出现的问题    

     *  方案一 :利用 @try @catch   

     *  方案二:利用 模型数组进行存储记录   

     *  方案二 :利用 observationInfo 里私有属性  

      *   

     */

    《方案一》

    /** *  方案一 :利用 @try @catch(只能针对删除多次KVO的情况下) 

    *  利用 @try @catc 不得不说这种方法真是很Low,不过很简单就可以实现。(对于初学者来说,如果不怕麻烦,确实可以使用这种方法)    这种方法只能针对多次删除KVO的处理,原理就是try catch可以捕获异常,不让程序catch。这样就实现了防止多次删除KVO。  

      在dealloc方法里面执行下面代码(我只是举个例子,监听的对象不一样,具体代码也不一样)

    -(void)dealloc{ 

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

    (解决方法1)

       AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;

       @try {

           [appDelegate removeObserver:self forKeyPath:@"kvoState"];

       }@catch (NSException *exception) {  

           NSLog(@"多次删除kvo报错了");

       }

    }

    有个简单的方法:给NSObject 增加一个分类,然后利用Run time 交换系统的 removeObserver方法,在里面添加 @try @catch。    

    步骤:创建一个类目NSObject+DSKVO,执行代码里面的步骤         然后可以在dealloc方法里面执行下面代码(我只是举个例子,监听的对象不一样,具体代码也不一样) 

      AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;        

     [appDelegate removeObserver:self forKeyPath:@"kvoState"];

    那么,那个类目里面的代码是这样的:(导入头文件:#import)

    (解决方法2)

    + (void)load{   

          [selfswitchMethod];}+ (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);

    }

    #pragma mark - 第一种方案

    ,利用@try @catch// 交换后的方法

    - (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath{  @try {//相对应解决方法1而已,只是把@try @catch 写在这里而已       

          [self removeDasen:observerforKeyPath:keyPath];   

          } @catch (NSException *exception) {

          } 

    }

    // 交换后的方法

    - (void)addDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath options:

    (NSKeyValueObservingOptions)options context:(void *)context{  

        [self addDasen:observerforKeyPath:keyPath options:options context:context];

    }

    这种方法 利用Run time交换系统的 removeObserver方法,在里面添加 @try @catch。相对上述那种解决方法来说,理解稍微难那么一点,但是,不需要移除kvo 的时候每次调用@try @catch(这样省了很多代码) 

    《方案二》(2) 方案二利用 模型数组 进行存储记录第一步 利用交换方法,拦截到需要的东西

    1,是在监听哪个对象。

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

    2,我们需要一个数组来存储这个模型。第三步 进行存储1,利用runtime 拦截到对象和keyPath,创建模型然后进行赋值模型相应的属性。

    2,然后存储进数组中去。

    第三步 存储之前的检索处理1,在存储之前,为了防止多次addObserver相同的属性,这个时候我们就可以,遍历数组,取出每个一个模型,然后取出模型中的对象,首先判断对象是否一致,然后判断keypath是否一致

    2,对于添加KVO监听:如果不一致那么就执行利用交换后方法执行addObserver方法。

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

    下面我讲介绍代码:

    + (void)load{  

      [selfswitchMethod];

    }

    + (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);

    }

    上述两个方法的代码同案例1 的一样(同样是新建一个类目NSObject+DSKVO),然后在写下面方法#pragma mark - 第二种方案,利用私有属性// 交换后的方法

    - (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath{   

        NSMutableArray *Observers = [DSObserversharedDSObserver];    

        ObserverData *userPathData = [selfobserverKeyPath:keyPath];   

        //如果有该key值那么进行删除    if (userPathData) {    

        [Observers removeObject:userPathData];   

       @try {//如果没有写@try @catch 的话,在dealloc 中,那个被监听的对象(appdelegate)必须要全局变量            [self removeDasen:observer forKeyPath:keyPath];       

     }    @catch (NSException *exception) {          

         }          

      }   

      return;

    }

    // 交换后的方法

    - (void)addDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{ 

       ObserverData *userPathData= [[ObserverDataalloc]initWithObjc:selfkey:keyPath];  

       NSMutableArray *Observers = [DSObserversharedDSObserver];   

       //如果没有注册,那么才进行注册  if (![self observerKeyPath:keyPath]) {     

        [ObserversaddObject:userPathData];      

       [selfaddDasen:observer forKeyPath:keyPath options:options context:context];  

      }

    }

    // 进行检索,判断是否已经存储了该Key值- (ObserverData *)observerKeyPath:(NSString *)keyPath{  NSMutableArray *Observers = [DSObserversharedDSObserver]; 

     for (ObserverData *datain Observers) {    

      if ([data.objcisEqual:self] && [data.keyPathisEqualToString:keyPath]) {   

           return data;     

       }  

      }    

    return nil;

    }这种情况还需要新建几个文件:DSObserver 、ObserverData

    —————————————————————————————————————————————————————

    #import@interface ObserverData :NSObject

    @property (nonatomic,strong)id objc;

    @property (nonatomic,copy)  NSString *keyPath;

    - (instancetype)initWithObjc:(id)objc key:(NSString *)key;

    @end#import "ObserverData.h"

    @implementation ObserverData

    - (instancetype)initWithObjc:(id)objc key:(NSString *)key{ 

     if (self = [superinit]) {      

     self.objc = objc;      

    self.keyPath = key;  

      }   

     return self;

    }

    @end---------------------------------------

    #import@interface DSObserver :NSMutableArray

    + (instancetype)sharedDSObserver;

    @end

    #import "DSObserver.h"

    @implementation DSObserver

    + (instancetype)sharedDSObserver

    {

    static id objc;

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

    objc = [NSMutableArrayarray];

    });

    return objc;

    }

    @end

    上述就是方案二了

    《方案三》

    利用 observationInfo 里私有属性

    第一步 简单介绍下observationInfo属性

    1,只要是继承与NSObject的对象都有observationInfo属性.

    2,observationInfo是系统通过分类给NSObject增加的属性。

    3,分类文件是NSKeyValueObserving.h这个文件

    4,这个属性中存储有属性的监听者,通知者,还有监听的keyPath,等等KVO相关的属性。

    5,observationInfo是一个void指针,指向一个包含所有观察者的一个标识信息对象,信息包含了每个监听的观察者,注册时设定的选项等。

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

    第二步 实现方案思路

    1,通过私有属性直接拿到当前对象所监听的keyPath

    2,判断keyPath有或者无来实现防止多次重复添加和删除KVO监听。

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

    比如这是监听两个属性状态下的数组

    4,NSKeyValueObservance属性简单说明

    _observer属性:里面放的是监听属性的通知这,也就是当属性改变的时候让哪个对象执行observeValueForKeyPath的对象。

    _property 里面的NSKeyValueProperty NSKeyValueProperty存储的有keyPath,其他属性我们用不到,暂时就不说了。

    5,拿出keyPath

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

    补充:NSKeyValueProperty我当时测试直接kvc取出来的时候发现取不出来,报错,后台直接取keyPath就可以,然后就直接取keyPath了,有知道原因的可以给我说下。

    + (void)load

    {

    [selfswitchMethod];

    }

    + (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);

    }

    #pragma mark - 第三种方案,利用私有属性

    // 交换后的方法

    - (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath

    {

    if ([self observerKeyPath:keyPath]) {

    [selfremoveDasen:observer forKeyPath:keyPath];

    }

    }

    // 交换后的方法

    - (void)addDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context

    {

    if (![self observerKeyPath:keyPath]) {

    [selfaddDasen:observer forKeyPath:keyPath options:options context:context];

    }

    }

    // 进行检索获取Key

    - (BOOL)observerKeyPath:(NSString *)key

    {

    id info =self.observationInfo;

    NSArray *array = [info valueForKey:@"_observances"];

    for (id objcin array) {

    id Properties = [objc valueForKeyPath:@"_property"];

    NSString *keyPath = [Properties valueForKeyPath:@"_keyPath"];

    if ([key isEqualToString:keyPath]) {

    return YES;

    }

    }

    return NO;

    }

    上述就是这个问题的解决方法

    参考文章:点击打开链接

    参考人员:tyh

    github地址:点击打开链接

    相关文章

      网友评论

        本文标题:iOS拦截系统KVO监听,防止多次删除和添加

        本文链接:https://www.haomeiwen.com/subject/tgueettx.html