序
上文中,初步探索了KVO的使用以及背后的实现原理,这次来自己实现一个KVO,一方面在过程中加深理解,另一方面可以感受一下使用场景。
根据上文总结,KVO的实现一共有3部分:
- 添加KVO监听方法
- KVO 通知实现方法
- KVO 移除方法
下面一步一步来进行实现
一、添加KVO监听
1.0 创建自定义方法
仿照KVO 方法之前,先看一下原生的方法,从中做一下借鉴:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; |
可见一共有4个重要参数
- 观察者 - observer - NSObject 类型
- 检测关键键 - keyPath - NSString 类型
- 选项 - options - NSKeyValueObservingOptions 类型
- 上下文 - 用来定位键值 - (void*) 指针类型
明白之后,写一个对NSObject 的分类,在分类里进行方法扩展,创建一个自定义的监听方法如下:
@interface NSObject (LJKVO) |
可见在options
做了自定义,可以简单的自定义一下:
typedef NS_OPTIONS(NSUInteger, LJKeyValueObservingOptions) { |
1.1 验证传入setter 有效性
第一件事就是判断当前对象的类里,是否有与这个keyPath
匹配setter
方法,得过了这一关,才允许添加观察,否则会抛出异常。
判断步骤如下:
拿到当前对象的类
获取
keyPath
对应的setter
方法编号。这里有个细节的地方是,setter 方法是首字母大写,比如属性是
name
,它的setter 方法变成setName
,所以需要对字符串做一下调整——添加set
、首字母大写——这些在代码里可以看见。去当前对象的父类——也就是类对象查找
keyPath
对应的方法实现,判断
setter
方法实现是否存在:- 如果实现,则通过;
- 找不到,即抛出异常
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{ |
1.2 动态生成自定义子类
大家知道原生的类添加KVO之后,会动态生成NSKVONotifying
的类,这个类继承自原来的类,有class
以及对属性的setter 方法、dealloc 方法等等。但是对我们自己写的——新诞生的动态子类,目前是光溜溜的来,但是自定义的步骤里面,它几乎是啥也没有,类似孙悟空从石头缝里蹦出来的一样。那么在这里,我们的流程是这样的:
- 动态生成类
- 注册类
- 给类添加
class
方法,即指向原类的方法 - 给类添加
setter
方法,基于keyPath
1.2.1 拼接子类名
还记得系统生成的动态子类,是以NSKVONotifying_ 作为前缀,我们也做一个类似的类如下
- 获取当前对象的类:
- 将自定义的拼接到前面
- 根据字符串生成子类
实现代码如下:
NSString *oldClassName = NSStringFromClass([self class]); |
得到的类,原来的类为Dog
, 新类将会是LJKVONotifying_Dog
1.2.2 判断是否存在动态子类
也要先判断这个类是否已经存在,以防止重复添加。如果已经存在就退出
// 防止重复创建生成新类 |
1.2.3 申请动态子类内存空间
这个比较简单,使用的是objc_allocateClassPair
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0); |
1.3.4 注册该动态子类
也就是讲这个新的 NSKVONotifying_xx
的类注册到(非元)类的散列表中
objc_registerClassPair(newClass); |
1.3.5 添加class 方法。即给新的动态子类,添加class
方法
首先是给这个类添加自定义的lj_class
方法,即使得该动态子类继承自原类。
这里涉及到给当前的动态子类的父类——即原来的类,建立联系
Class lj_class(id self, SEL _cmd){ |
那么,接下来,将这个类方法,添加到子类的方法列表里:
SEL classSEL = NSSelectorFromString(@"class"); |
1.3.6 添加自定义的setter
方法。
由于KVO 主要观察的是 setter
方法,所以需要写一个自定义的 setter
方法。
setter 方法的核心是,当这个动态子类接收到消息后,转发给原来的类——也就是其父类,告知它,现在属性有变化了,具体的实现,是把改动的值change
通过消息发送(objc_msgSend) 发送过去。
好的,下面来完成这个lj_setter
方法
从
setter
方法,获得完整的方法名keyPath
static void lj_setter(id self, id newValue){
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
/****/
}
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length-4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}向父类发送新的值,此时需要自定义一个结构体,然后使用objc_msgSendSuper 发送
id oldValue = [self valueForKey:keyPath];
void (*lj_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// void /* struct objc_super *super, SEL op, ... */
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
//objc_msgSendSuper(&superStruct,_cmd,newValue)
lj_msgSendSuper(&superStruct,_cmd,newValue);获取观察者。这里的观察者,为了避免全局冗余参数参数,提前使用关联对象与本类进行绑定,这一点在后面会介绍。
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLJKVOAssiociateKey));
接下来,处理新的旧的值,将
newValue
的改变 该遍赋值给change
,并发送给类,完成消息的发送for (LJKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
// 对新旧值进行处理
if (info.options & LJKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & LJKeyValueObservingOptionOld) {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
}
}
// 2: 消息发送给观察者
SEL observerSEL = @selector(lj_observeValueForKeyPath:ofObject:change:context:);
objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
});
}
}可以看到观察者使用了
LJKVOInfo
, 这里我对观察者,做了模型的处理,更方便接耦。里面包含了众多参数如下:
@interface LJKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) LJKeyValueObservingOptions options;注意⚠️,这里有个坑点,是关于
observer
!由于原对象持有observer,observer 在添加关联对象时又持有self,self 又持有原对象,会导致循环引用——所以这里observer 必须使用weak 弱引用,防止循环引用
好了,现在的setter
方法的步骤已经完成,把它添加到动态子类里
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath)); |
1.3 isa 转向
还记得原生的对象添加了KVO后,会将isa swizzling
,将isa
偷偷指向新的动态子类,委托后者对键值的变化进行消息发送,我们现在就是要做这个。
object_setClass(self, newClass); |
一行代码搞定……
1.4 保存观察者信息
好了,动态子类也生成类,isa
也转向了,是不是大功告成了?不,还少了一步,把当前的监控者保存一下,不然以后无法找到他进行消息传递
LJKVOInfo *info = [[LJKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options]; |
二、实现KVO监听方法
这部分是最简单的,由开发者根据业务实现内容;
还是打印一下我写的
- (void)lj_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context{ |
三、移除KVO 监听
移除KVO 与添加KVO 的进程是相反的,主要工作有:从观察者群里移除指定keyPath
的观察者、isa 转向(指回原类)
3.1 获取观察者群体
还是用关联对象获取
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kljKVOAssiociateKey)); |
3.1 移除观察者
这里移除观察者,是根据remove
方法传入的keyPath 进行匹配,如果匹配一致,则进行移除
for (LJKVOInfo *info in observerArr) { |
3.2 isa 转向
通过object_setClass
将isa
指回去
|
四、改进——函数式编程
上述的步骤基本完成了自定义的KVO,但是使用起来太过冗余,每次使用,都得调用 addObserver、observeValueForKeyPath以及removeObserver 三个函数,使用起来非常的冗余,是否有改进的空间呢?
这里,使用block
的思想,使得更加函数的调用的聚合
4.1 定义block函数类型
我们把添加的四大要素,都加入到block类型里,以供业务代码调用,具体如下:
typedef void(^LJKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue); |
4.2 改进添加观察者方法
先把观察者模型的初始化方法,改进称为
block
, 方便回调- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(LJKVOBlock)block{
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}函数声明如下:
- (void)lj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LJKVOBlock)block;
函数实现改进如下:
- (void)lj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(LJKVOBlock)block{
// 1: 验证是否存在setter方法 : 不让实例进来
[self judgeSetterMethodFromKeyPath:keyPath];
// 2: 动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3: isa的指向 : ljKVONotifying_ljPerson
object_setClass(self, newClass);
// 4: 保存信息
ljInfo *info = [[ljInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kljKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kljKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
}
4.3 具体使用
使用起来,就很简洁了,一个block
搞定
self.d = [[Dog alloc] init]; |
五、自动销毁机制
上面第四部实现了block 引用,的确大大提升了业务代码的使用效率,但是还是需要自动执行移除观察,以及isa 的转向回去。
是否有更多优化的空间,将移动观察做的更加高效呢?
当然可以!还记得动态子类生成后,也动态添加了dealloc 方法,那么,还是做dealloc 方法上做文章吧。
思路:自定义一个dalloc ,在里面实现对对象的KVO移除,在load
时将其与系统dealloc 进行方法交换
5.1 写自定义dealloc
自己写一个dealloc,主要实现isa
的转向,在系统dealloc 进行之前,将当前对象的isa
指向原来的类。
代码如下:
- (void)myDealloc{ |
5.2 将新旧方法进行交换
这里依然使用runtime 的method swizzling 进行。
+ (BOOL)kc_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL { |
5.3 在load 中执行hook 方法及交换
由于dealloc 为系统方法,在ARC 不允许通过 @selector(dealloc)
写入
那么还是使用runtime 特性,使用NSSelectorFromString(@"dealloc")
来拿到,并进行交换。
这些交换,写到load
里面,编译时即可完成
+ (void)load{ |
六、总结
总的来说,自定义的KVO 和自定义的KVC 很类似,一步一步,生成子类,完善方法,isa 转向;而移除时,倒叙进行,isa 指回去之后,移除观察就可以了,而动态子类会保留,以便下次使用。
关于自定义KVO 的Demo 已经上传至我的Github ,欢迎下载参考