不偷懒,不偷懒。今天带来一个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