IOS —— KVO的一个小封装

不偷懒,不偷懒。今天带来一个KVO封装,以及封装过程中捡起来的知识


那么首先,KVO是什么呢?

Key - Value - Observer 的缩写,意为键值对的观察。

实际上的作用就是用来观察键值对的变化,以及观察到变化后应该执行些什么操作

怎么用?苹果早就帮我们封装好了

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

如果有需要观察某键值的变化时,我们需要addObserver添加一个观察者,并且在不需要的时候removerObserver去除他。这是一个需要分俩步的操作

今天封装的这个方法便是实现一个简单的需求:

为一个类添加一个观察者,当监控到他属性的值发生变化时,执行一个block后销毁观察者

创建分类、声明block、声明方法

typedef void(^xgKvoBlock)(void);

@interface NSObject (XGKVO)

- (void)xgObserver:(NSObject *)observer keyPath:(NSString *)keyPath block:(xgKvoBlock)block;

@end  

这个是.h文件。

在铺开.m文件的代码前。我们先讲一讲观察者observer以及观察者对象keyPath这俩个家伙

一个observer可以对应多个keyPath。

一个keyPath也可以对应多个observer。

这俩句话听起起来念起来都很奇怪。

照常举个例就明白了:

现在有一个Person类,类里有name、sex、height等属性。

1.我们可以在ViewController添加观察者observer监听name属性,也可以利用观察者observer同时的监听sex属性

2.我们可以在ViewController里添加观察者observer监听他的name属性,也可以在ViewController2里添加观察者observer他的name属性

这俩句话也应对了上面加粗的俩句

他们是一一对应的,这意味着如果要完成刚才提到的需求,我们需要完整的获取所有的KeyPath以及Observer并一起remove(释放)他们

那么这时候需求明确了 ,Do it!

首先我们需要一个存储block的字典对象,以及一个存储KVO 中observer以及keyPath的字典对象。并且在内部约束存储对象类型

@property (nonatomic , strong) NSMutableDictionary <NSString *,xgKvoBlock> *dict;
@property (nonatomic , strong) NSMutableDictionary <NSString *,NSMutableArray *>*kvoDict;

然后实现类方法

- (void)xgObserver:(NSObject *)observer keyPath:(NSString *)keyPath block:(xgKvoBlock)block;

在引用observer.dict[keyPath]的时候我们会发现,报警告:告诉我们实例对象并没有生成。这是分类方法中常见的警告。

分类方法中@property是不会帮我们生成实例对象,所以我们必须利用别的方式实现分类对象中的get、set方法。

这里就直接铺一份代码,代码中有相应关键词的注释!

- (NSMutableDictionary<NSString *,xgKvoBlock> *)dict
{
    /*
     objc_setAssocaitedObject 以及objc_getAssocaitedObjects方法 手动实现实例对象的创建
     objc_getAssocaitedObject 参数1:调用者 参数2:关联的键值对象方法
     objc_setAssocatiedObject 参数1:调用者 参数2:关联的键值对象方法 参数3:需要设置对象的属性
     参数4:设置@property <(nonatomic , strong )> 尖括号中的属性
     */
    NSMutableDictionary *tmpDict = objc_getAssociatedObject(self, @selector(dict));
    if (!tmpDict) {
        tmpDict = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, @selector(dict), tmpDict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return tmpDict;
}

- (NSMutableDictionary<NSString *,NSMutableArray *> *)kvoDict
{
    NSMutableDictionary *tmpDict = objc_getAssociatedObject(self, @selector(kvoDict));
    if (!tmpDict) {
        tmpDict = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, @selector(kvoDict), tmpDict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return tmpDict;
}

好的这下没问题了。那么我们先实现第一步利用方法添加观察者observer,并执行系统添加观察者的方法

- (void)xgObserver:(NSObject *)observer keyPath:(NSString *)keyPath block:(xgKvoBlock)block
{
    //observer 观察者
    //keyPath 观察者对象
    observer.dict[keyPath] = block;
    [self addObserver:observer forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:nil];
}

 

接下来使用这么个方法

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

这时候脑袋大了,这方法有什么用呢?从释意里截一段

/* Given that the receiver has been registered as an observer of the value at a key path relative to an object, be notified of a change to that value */

在KVO当中,被观察者与观察者应该先建立关系,当被观察的特定属性改变时,立刻通知观察者,建立联系并调用此方法。

所以明确的一点是,当监听的对象值变化时,block的调用就是在此。

当监听的值发生改变时,获取字典里keyPath键值对应的block代码。执行他。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    //执行block
    xgKvoBlock block = self.dict[keyPath];
    if (self.dict[keyPath]) {

        block();
    }
}

那么第一步完成了之后,开始寻思第二步的编码了。当执行完block之后,销毁该观察者。

观察者和观察者对象不止一个,既然不止一个。我们就一起获取他们,并且存到一个可变的array后一起销毁他们。

主方法里通过键值keypath获得观察者对象,以及观察者,一起加入数组中。

    NSMutableArray *arr = self.kvoDict[keyPath];
    if (!arr) {
        arr = [NSMutableArray array];
        self.kvoDict[keyPath] = arr;
    }
    [arr addObject:observer];

 

至于销毁肯定会有人说,我知道我知道,dealloc方法!重写他就好了。嘟嘟嘟

这是错误的,dealloc方法作为系统的根类。贸贸然重写是会导致未知报错的。所以我们自己编一个伪dealloc方法。并且在该分类方法中替换掉dealloc方法

 static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        method_exchangeImplementations(class_getInstanceMethod([self class], @selector(xgDealloc)), class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc")));
    });

这里我们另开一条线程,单独执行一次该替换方法的语句。至于代码的细节。读者英文就能懂了吧。。。

接下来实现的是dealloc方法

- (void)xgDealloc
{
        [self.kvoDict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableArray * _Nonnull obj, BOOL * _Nonnull stop) {
            
            NSMutableArray *arr = self.kvoDict[key];
            [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                [self removeObserver:obj forKeyPath:key];
                
            }];
        }];
    [self xgDealloc];
}

方法中的enumerateKeysAndObjectsUsingBlock和enumerateObjectsUsingBlock分别类等于forin 遍历/ for循环遍历

那么剩下的就简单易懂了,遍历每个数组,销毁他

这时候应该已经大功告成了吧?

其实并不是,这时候运行会引发不知名的死循环。原因出在dealloc中。报错提示也是简单易懂

kvodict为空指针,空指针销毁怎么可能不报错呢?正如上文提及到的。如果在分类方法中,实例的创建是需要手动的。自然而然此处也受到实例化失败的影响。

所以此处我们必须得先加一个判断(bool),并且修改下dealloc方法

- (BOOL)isKvoDict
{
    if (objc_getAssociatedObject(self, @selector(kvoDict))) {
        return YES;
    }else{
        return  NO;
    }
}
- (void)xgDealloc
{
    if ([self isKvoDict]) {
        [self.kvoDict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableArray * _Nonnull obj, BOOL * _Nonnull stop) {
            
            NSMutableArray *arr = self.kvoDict[key];
            [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop{

                [self removeObserver:obj forKeyPath:key];
            }];
        }];
    }
    
    [self xgDealloc];
}

如果kvoDict有数据的话,开始判断是否为空指针,当不为空指针时销毁。


偷懒了好一阵子,捡起来捡起来捡起来。

这里涉及的部分知识点,实际上与我们日常接触都差不了多少,很多也只是换一种形式。只是一些需要注意的地方要注意。

分类方法中实例问题啊,销毁对象,根类改变等等。

那么今明俩天继续更新~over

posted @ 2018-12-24 10:00  幽幽幽瓜  阅读(295)  评论(0编辑  收藏  举报