阅读 268

iOS底层探索之KVO(三)—自定义KVO

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

回顾

在前两篇博客中,已经介绍了KVO的相关操作,还有KVO的底层逻辑是通过动态生成子类,重写父类的方法实现的,那么我们如何自定义一个KVO呢?

iOS底层探索之KVO(一)—KVO简介

iOS底层探索之KVO(二)—KVO原理分析

1. 前期分析

系统的KVO是在NSObject的上面拓展了一些能力,如下图所示:

在这里插入图片描述

系统的KVO使用的三部曲是:

  • 添加监听addObserver
  • 监听回调observeValueForKeyPath
  • 移除监听removeObserver

我们也仿照系统的API自定义一个KVO,如下:

@interface NSObject (JP_KVO)

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end
复制代码

我们自定义KVO是对属性进行观察,那么成员变量是没有setter方法的,所以第一步就得过滤掉成员变量。如何动态生成子类,改变isa的指向,保存观察者,步骤大致如下:

  1. 验证是否存在setter方法 : 不让成员变量(实例变量)进来
  2. 动态生成子类
  3. 改变子类的isa的指向
  4. 保存我们的观察者
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    
    // 1: 验证是否存在setter方法 : 不让实例进来
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 动态生成子类
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: isa的指向
    object_setClass(self, newClass);
    // 4: 保存观察者
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(KJPKVOAssiociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
复制代码

2. 验证是否存在setter方法

  • judgeSetterMethodFromKeyPath:keyPath
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
    Class superClass    = object_getClass(self);
    SEL setterSeletor   = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"不好意思没有当前%@的setter",keyPath] userInfo:nil];
    }
}
复制代码

判断是否有setter方法,没有就抛出异常信息

3. 动态生成子类

- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kjpKVOPrefix,oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // 防止重复创建生成新类
    if (newClass) return newClass;
    /**
     * 如果内存不存在,创建生成
     * 参数一: 父类
     * 参数二: 新类的名字
     * 参数三: 新类的开辟的额外空间
     */
    // 2.1 : 申请类
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    // 2.2 : 注册类
    objc_registerClassPair(newClass);
    // 2.3.1 : 添加class : class的指向是JPPerson
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)jp_class, classTypes);
    // 2.3.2 : 添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)jp_setter, setterTypes);
    return newClass;
}
复制代码
  • JP_class 返回父类信息
Class JP_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
复制代码
  • 判断是否已经创建了子类,没有就创建
  • 申请类
  • 注册类
  • newClass不存在则调用objc_allocateClassPair创建kvo子类,并且重写- class方法
  • 添加setter
  • 返回newClass

4. 创建setter

创建setter方法主要是分两部分:调用父类方法以及发送通知

static void jp_setter(id self,SEL _cmd,id newValue){
    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    void (*jp_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)
    jp_msgSendSuper(&superStruct,_cmd,newValue);
    
    // 既然观察到了,下一步不就是回调 -- 让我们的观察者调用
    // - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    // 1: 拿到观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey));
    
    // 2: 消息发送给观察者
    SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    objc_msgSend(observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
}

复制代码
  • 调用父类方法可以通过objc_msgSendSuper实现,调用父类的setter(也可以通过performSelector调用

  • 通知观察者keypath可以通过_cmd转换获取,objectselfchange也可以获取到,context可以先不传。那么核心就是observer的获取。

  • 通过关联对象的方式,把observer进行存储

  • 拿到观察者了,就把消息发送给观察者,进行信息的回调处理

  • getterForSetter

#pragma mark - 从set方法获取getter方法的名称 set<Key>:===> key
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];
}
复制代码

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

文章分类
iOS
文章标签