【底层探索】- KVO(下)自定义KVO


上文中,初步探索了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)
- (void)lj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LJKeyValueObservingOptions)options context:(nullable void *)context;

可见在options 做了自定义,可以简单的自定义一下:

typedef NS_OPTIONS(NSUInteger, LJKeyValueObservingOptions) {

LJKeyValueObservingOptionNew = 0x01,
LJKeyValueObservingOptionOld = 0x02,
};

1.1 验证传入setter 有效性

第一件事就是判断当前对象的类里,是否有与这个keyPath匹配setter 方法,得过了这一关,才允许添加观察,否则会抛出异常。

判断步骤如下:

  1. 拿到当前对象的类

  2. 获取keyPath 对应的setter方法编号。

    这里有个细节的地方是,setter 方法是首字母大写,比如属性是name,它的setter 方法变成setName,所以需要对字符串做一下调整——添加set、首字母大写——这些在代码里可以看见。

  3. 去当前对象的父类——也就是类对象查找keyPath 对应的方法实现,

  4. 判断setter方法实现是否存在:

    1. 如果实现,则通过;
    2. 找不到,即抛出异常
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
/*Step 1*/
Class superClass = object_getClass(self);
/*Step 2*/
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
/*Step 3*/
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
/*Step 4*/
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没有当前%@的setter",keyPath] userInfo:nil];
}
}

/* 对setter 方法进行字符串微调
1、添加set字符
2、首字母大写
如name 属性,其setter方法为 setName
*/
static NSString *setterForGetter(NSString *getter){

if (getter.length <= 0) { return nil;}

NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *leaveString = [getter substringFromIndex:1];

return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}

1.2 动态生成自定义子类

大家知道原生的类添加KVO之后,会动态生成NSKVONotifying 的类,这个类继承自原来的类,有class 以及对属性的setter 方法、dealloc 方法等等。但是对我们自己写的——新诞生的动态子类,目前是光溜溜的来,但是自定义的步骤里面,它几乎是啥也没有,类似孙悟空从石头缝里蹦出来的一样。那么在这里,我们的流程是这样的:

  • 动态生成类
  • 注册类
  • 给类添加class 方法,即指向原类的方法
  • 给类添加setter 方法,基于keyPath

1.2.1 拼接子类名

还记得系统生成的动态子类,是以NSKVONotifying_ 作为前缀,我们也做一个类似的类如下

  • 获取当前对象的类:
  • 将自定义的拼接到前面
  • 根据字符串生成子类

实现代码如下:

NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",@"LJKVONotifying_",oldClassName];
Class newClass = NSClassFromString(newClassName);

得到的类,原来的类为Dog, 新类将会是LJKVONotifying_Dog

1.2.2 判断是否存在动态子类

也要先判断这个类是否已经存在,以防止重复添加。如果已经存在就退出

// 防止重复创建生成新类
if (newClass) return newClass;

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){
return class_getSuperclass(object_getClass(self));
}

那么,接下来,将这个类方法,添加到子类的方法列表里:

SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)lj_class, classTypes);

1.3.6 添加自定义的setter 方法。

由于KVO 主要观察的是 setter 方法,所以需要写一个自定义的 setter 方法。

setter 方法的核心是,当这个动态子类接收到消息后,转发给原来的类——也就是其父类,告知它,现在属性有变化了,具体的实现,是把改动的值change 通过消息发送(objc_msgSend) 发送过去。

好的,下面来完成这个lj_setter 方法

  1. 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];
    }
  2. 向父类发送新的值,此时需要自定义一个结构体,然后使用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);
  3. 获取观察者。这里的观察者,为了避免全局冗余参数参数,提前使用关联对象与本类进行绑定,这一点在后面会介绍。

    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLJKVOAssiociateKey));
  4. 接下来,处理新的旧的值,将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));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)lj_setter, setterTypes);

1.3 isa 转向

还记得原生的对象添加了KVO后,会将isa swizzling ,将isa 偷偷指向新的动态子类,委托后者对键值的变化进行消息发送,我们现在就是要做这个。

object_setClass(self, newClass);

一行代码搞定……

1.4 保存观察者信息

好了,动态子类也生成类,isa 也转向了,是不是大功告成了?不,还少了一步,把当前的监控者保存一下,不然以后无法找到他进行消息传递

LJKVOInfo *info = [[LJKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLJKVOAssiociateKey));

if (!observerArr) {
observerArr = [NSMutableArray arrayWithCapacity:1];
[observerArr addObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLJKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

二、实现KVO监听方法

这部分是最简单的,由开发者根据业务实现内容;

还是打印一下我写的

- (void)lj_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context{
NSLog(@"change %@", change);
}

三、移除KVO 监听

移除KVO 与添加KVO 的进程是相反的,主要工作有:从观察者群里移除指定keyPath 的观察者、isa 转向(指回原类)

3.1 获取观察者群体

还是用关联对象获取

NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kljKVOAssiociateKey));
if (observerArr.count<=0) {
return;
}

3.1 移除观察者

这里移除观察者,是根据remove 方法传入的keyPath 进行匹配,如果匹配一致,则进行移除

for (LJKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLJKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}

3.2 isa 转向

通过object_setClassisa 指回去


if (observerArr.count<=0) {
// 指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}

四、改进——函数式编程

上述的步骤基本完成了自定义的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];
[self.d lj_addObserver:self forKeyPath:@"petName" block:^(id _Nonnull observer, NSString * _Nonnull keyPath, id _Nonnull oldValue, id _Nonnull newValue) {
NSLog(@"%@-%@",oldValue,newValue);
}];

五、自动销毁机制

上面第四部实现了block 引用,的确大大提升了业务代码的使用效率,但是还是需要自动执行移除观察,以及isa 的转向回去。

是否有更多优化的空间,将移动观察做的更加高效呢?

当然可以!还记得动态子类生成后,也动态添加了dealloc 方法,那么,还是做dealloc 方法上做文章吧。

思路:自定义一个dalloc ,在里面实现对对象的KVO移除,在load 时将其与系统dealloc 进行方法交换

5.1 写自定义dealloc

自己写一个dealloc,主要实现isa 的转向,在系统dealloc 进行之前,将当前对象的isa 指向原来的类。

代码如下:

- (void)myDealloc{

Class superClass = [self class];
object_setClass(self, superClass);
[self myDealloc];
}

5.2 将新旧方法进行交换

这里依然使用runtime 的method swizzling 进行。

+ (BOOL)kc_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
Class cls = self;
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);

if (!swiMethod) {
return NO;
}
if (!oriMethod) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}

BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
return YES;
}

5.3 在load 中执行hook 方法及交换

由于dealloc 为系统方法,在ARC 不允许通过 @selector(dealloc) 写入

那么还是使用runtime 特性,使用NSSelectorFromString(@"dealloc") 来拿到,并进行交换。

这些交换,写到load 里面,编译时即可完成

+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

[self kc_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(myDealloc)];
});
}

六、总结

总的来说,自定义的KVO 和自定义的KVC 很类似,一步一步,生成子类,完善方法,isa 转向;而移除时,倒叙进行,isa 指回去之后,移除观察就可以了,而动态子类会保留,以便下次使用。

关于自定义KVO 的Demo 已经上传至我的Github ,欢迎下载参考


文章作者: 李佳
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李佳 !
评论
 上一篇
【底层探索】- 多线程(二)GCD应用 【底层探索】- 多线程(二)GCD应用
一、面试题解析1.1 第一题A、提问:以下代码,会打印什么? dispatch_queue_t queue = dispatch_queue_create("lj", DISPATCH_QUEUE_SERIAL);NSL
2020-04-05 李佳
下一篇 
【数据结构与算法】-(4)双向链表和双向循环链表 【数据结构与算法】-(4)双向链表和双向循环链表
【数据结构与算法】-(1)基础篇 【数据结构与算法】-(2)线性表基础 【数据结构与算法】-(3)循环链表(单向) 【数据结构与算法】-(4)双向链表和双向循环链表 【数据结构与算法】-(5)链表面试题解析 【数据结构与算法】
2020-04-03 李佳
  目录