原创文章首发本人博客:blog.cocosdever.com/2019/07/03/…git
OC为用户提供了一套观察者模式(KVO), 当对象的某些属性发生变化以后, 就会向全部观察者(observer)广播消息, 具体的KVO基本用法这里就不说了. 下面主要说一下为系统的KVO功能添加block的思路, 先看一下最终的API:github
UIView *v = [[UIView alloc] init];
NSObject *obj = [[NSObject alloc] init];
[obj cc_easyObserve:v forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew block:^(id object, NSDictionary<NSKeyValueChangeKey,id> *change) {
NSLog(@"hello");
}];
复制代码
要添加block功能到系统的KVO中, 首先要作的事情是传这个block指针能传入KVO中, 在消息广播的时候又能把这个block带回来.先看一下系统的API: api
// NSObject类
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
// 观察者(observer)必须实现下面方法才能接收到广播
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
复制代码
其中有一个参数是content, 容许传入void *
类型的指针, 因此咱们能够直接把用户传入的block转成void *
类型, 传入KVO中, 这样当消息进行广播的时候, 就能够从这个context中获得block的地址, 再调用block便可.安全
通过上面分析可知, 要为系统的KVO功能添加block特性理论上是可行的, 下面就开始代码的实现部分.函数
添加block属性就是为了方便使用系统的KVO功能, 因此咱们首选分类(Category)来实现, 直接扩展NSObject, 这样全部的对象都有便捷的操做了.ui
// NSObject+CCEasyKVO.h
/** @abstract 回调函数 @param object 状态发生变化的对象(被观察者) @param change 发生变化的信息 */
typedef void (^CC_EasyBlock)(id object, NSDictionary<NSKeyValueChangeKey, id> *change);
@interface NSObject (CCEasyKVO)
/** 简易KVO @param observe 被观察者 @param keyPath key @param options options @param block 回调函数 */
- (void)cc_easyObserve:(id)observe forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(CC_EasyBlock) block;
- (void)cc_easyRemoveAllKVO;
@end
复制代码
上面就是咱们的头文件部分, 比较简单, 主要就是提供了一套便捷KVO的api, 其中CC_EasyBlock
就是用户须要传入的block.spa
接下来要解决一个重要的问题. 咱们可否直接使用当前被分类的对象做为观者者直接观察observe
呢? 答案是否认的, 这个你能够本身尝试一下. 缘由就是当用户在类里也实现了系统KVO接受广播的方法observeValueForKeyPath...
时, 调用的其实是分类的代码, 用户的类里就没法再收到系统的广播了. 为了解决这个问题, 咱们能够在分类里使用自定义的类(CCInternalObserver)来做为观察者, 这样就算用户给本身的类实现了接受广播的方法, 也不影响咱们的代码. 咱们在CCInternalObserver里实现observeValueForKeyPath...
, 当广播到来时, 调用context指向的block.指针
如何避免用户传入的block内存被释放? 简单说就是如何管理block内存? oc的block一共有三种, 分别是全局块NSGlobalBlock
, 堆块NSMallocBlock
, 栈块NSStackBlock
. 这里顺便简单介绍一下他们的区别:code
(1) block类型区别
没有引用外部任何变量(static变量除外), 建立的就是NSGlobalBlock;
除了NSGlobalBlock, 其余建立的时候就是NSStackBlock, 赋值给strong类型的变量以后就是NSMallocBlock, 这里也称之为copy操做;
在符合NSStatckBlock的条件下, 能够经过两种方法获取NSStatckBlock:
1. 在调用方法时建立匿名block, 在方法内部获得的block变量是NSStatckBlock
2. 建立的block赋值给__weak变量.
(2) 内存管理
NSStackBlock类型的块, 会随栈内存释放而释放, 使用的时候须要先用strong变量存储起来, 不然将crash;
NSGlobalBlock类型的块, 不会被释放; NSMallocBlock类型和其余引用类型同样, 没人引用就会被释放;
除了NSStackBlock类型, 其余类型赋值给变量的时候都不会重复copy.
复制代码
用户传入的block多是三种类型之一, 为了不内存出问题, 在转成void *的时候就须要作一点额外的处理, 才能传给系统的KVO:server
// 用户传入的block多是NSStackBlock, 因此在转为无类型指针的时候必须将全部权转移给CoreFoundatin层, 这样一来block类型会转为NSMallocBlock并被持有, 也就安全了
[observe addObserver:self.observer forKeyPath:keyPath options:options context:(__bridge_retained void *)block];
复制代码
顺便说一句, self.observer
就是上面说的CCInternalObserver
: )
第三个问题就是如何注销观察者. 系统的KVO功能还有一个麻烦的地方就是每次用完都须要手动注销, 不然被观察的对象一会向那些已经注册过的观察者广播消息时, 若是观察者被内存被释放了就会引起EXC_BAD_ACCESS
, 因此当观察者被释放时, 要及时把观察者(observer)从被观察者(observe)身上移除. 为了解决这个问题, 能够在CCInternalObserver
建立一个哈希表, 存放全部被观察者(observe), 并重写CCInternalObserver
的dealoc
方法, 移除全部观察.
上面已经把核心的代码细节都说完了. 完整的代码我已经作成一个Category NSObject+CCEasyKVO.h
, 直接引入项目就可使用了. CCEasyKVO源码