美文网首页面试基础
自己实现KVO(代理方式)

自己实现KVO(代理方式)

作者: 晨曦中的花豹 | 来源:发表于2021-07-12 20:08 被阅读0次

之前已经了解系统实现KVO的过程,这篇文章主要说一下自己实现KVO的思路
开始之前推荐大家看一下这篇文章,很多细节这个里面说的很清楚,并且实现了block进行回调,我这里只是为了模仿系统实现KVO,之后我也会更新Block的版本
https://tech.glowing.com/cn/implement-kvo/
下面是我的具体实现,及每一步的作用:

//
//  NSObject+LXC_KVO.m
//  leetCode
//
//  Created by 刘晓晨 on 2021/7/9.
//

#import "NSObject+LXC_KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import "LXCObservationInfo.h"

static NSString *kvo_class_prefix = @"KVOClass_";
const void *kvo_observer = &kvo_observer;
const void *kvo_info = &kvo_info;


@implementation NSObject (LXC_KVO)

- (void)lxc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
    //原始类
    Class oldClass = object_getClass(self);
    // 拿到原始类名
    NSString *oldClassName = NSStringFromClass(oldClass);
    //原始SEL
    SEL oldSelector = NSSelectorFromString(setterFromGetter(keyPath));
    // 1.创建子类
    Class kvoClass =  [self makeKvoClassWithOriginalClassName:oldClassName];
    
    //2.重写set方法(本质是替换方法的实现)
    // cls 添加新方法的类;  name 表示selector的方法名称;  imp 指向一个方法的实现; types 表示我们要添加方法的返回值和参数:  v 代表函数的返回值类型 void,  :@ 表示调用setName:函数的时的参数
    Method clazzMethod = class_getInstanceMethod(oldClass, oldSelector);
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClass, oldSelector, (IMP)kvo_setName, types);
    
    // 3.修改isa指针,使得self->isa指向子类
    object_setClass(self, kvoClass);
    
    // 4.保存观察者对象(所谓的循环引用根源在此)
    objc_setAssociatedObject(self, kvo_observer, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    // 5.保存上下文及options(系统如何保存目前不知道,希望知道的朋友指教)
    LXCObservationInfo *info = [[LXCObservationInfo alloc] init];
    info.context = context;
    info.options = options;
    objc_setAssociatedObject(self, kvo_info, info, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

//创建子类
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName {
    NSString *kvoClazzName = [kvo_class_prefix stringByAppendingString:originalClazzName];
    Class clazz = NSClassFromString(kvoClazzName);
    
    if (clazz) {
        return clazz;
    }
    
    Class originalClazz = object_getClass(self);
    
    //让新的Class继承自原始类
    Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
    
    //仿苹果隐藏子类(及重写本类的class对象方法)
    Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
    
    //注册子类
    objc_registerClassPair(kvoClazz);
    
    return kvoClazz;
}

//重写class方法
Class kvo_class(id self, SEL _cmd) {
    return class_getSuperclass(object_getClass(self));
}

//统一管理setter方法
void kvo_setName(id self, SEL _cmd, id newName)
{
    // 获取kvo类型
    id class = object_getClass(self);
    
    // 拿到观察者
    id observer = objc_getAssociatedObject(self, kvo_observer);
    //信息
    LXCObservationInfo *info = objc_getAssociatedObject(self, kvo_info);
    
    //判断是否监听变化之前
    id  oldValue;
    if (info.options && NSKeyValueObservingOptionOld) {
        //1.获取原值
        oldValue = ((id (*)(id,SEL))objc_msgSend)(self, NSSelectorFromString(getterFromSetter(NSStringFromSelector(_cmd))));
    }
    
    // 调用父类的方法(此处还有一种方式是修改self isa 指向原始类,修改后在修改为 子类,这里使用的是系统实现super的方式,顺便可以了解下super和self的区别)
    Class super_class = class_getSuperclass(class);
    struct objc_super * _Nonnull super_struct = malloc(sizeof(struct objc_super));
    super_struct->receiver = self;
    super_struct->super_class = super_class;
    objc_msgSendSuper(super_struct, _cmd,newName);
    
    //判断需要返回哪些值
    id  newValue;
    if (info.options && NSKeyValueObservingOptionNew) {
        //1.获取修改值
        newValue = ((id (*)(id,SEL))objc_msgSend)(self, NSSelectorFromString(getterFromSetter(NSStringFromSelector(_cmd))));
    }
    NSDictionary *dict = [[NSMutableDictionary alloc] init];
    if (oldValue != nil) {
        [dict setValue:oldValue forKey:NSKeyValueChangeOldKey];
    }
    if (newValue != nil) {
        [dict setValue:newValue forKey:NSKeyValueChangeNewKey];
    }
    //2.发送给监听者
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),getterFromSetter(NSStringFromSelector(_cmd)),observer,dict,info.context);
}

//通过属性获取setter字符串
NSString* setterFromGetter(NSString *key) {
    if (key.length > 0) {
        NSString *resultString = [key stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] capitalizedString]];
        return [NSString stringWithFormat:@"set%@:",resultString];
    }
    return nil;
}

//通过setter 获取getter
NSString* getterFromSetter(NSString *key) {
    if (key.length > 0) {
        NSString *resultString = [key substringFromIndex:3];
        resultString = [resultString substringToIndex:resultString.length - 1];
        return [resultString lowercaseString];
    }
    return nil;
}


@end

上面kvo_setName中调用父类的setter方法使用的是系统提供的方法,这里还有一种可以通过修改isa去调用object_setClass调用完后在重新指向子类
最后做下测试:

- (void)viewDidLoad {
    [super viewDidLoad];
    _person = [Person new];
    _person.ageee = @"123";
    NSLog(@"%p ---- %p",[_person class],object_getClassName(_person));
    [_person lxc_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:sizeContext];
    NSLog(@"%p ---- %p",[_person class],object_getClassName(_person));
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == sizeContext) {
        if ([keyPath isEqualToString:@"age"]) {
            if (change[NSKeyValueChangeOldKey] != nil) {
                NSLog(@"old = %@",change[NSKeyValueChangeOldKey]);
            }
            if (change[NSKeyValueChangeNewKey] != nil) {
                NSLog(@"new = %@",change[NSKeyValueChangeNewKey]);
            }
            
        }
    }
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    static int i = 0;
    _person.age = [NSString stringWithFormat:@"%d",i++];
}

下一篇,准备参照开始说的文章,做一个block回调的KVO,感谢大家支持!!!

相关文章

  • 自己实现KVO(代理方式)

    之前已经了解系统实现KVO的过程,这篇文章主要说一下自己实现KVO的思路开始之前推荐大家看一下这篇文章,很多细节这...

  • iOS六种传值方式之代理模式

    实现方式有六种,分别是:代理传值、观察者模式(KVO)、通知、单例模式、block以及非代理。将思路一一总结如下:...

  • iOS 代理、通知、KVO

    1.代理delegate 2.通知NSNotification 3.KVO KVO的实现依赖于 Objective...

  • iOS开发面试攻略(KVO、KVC、多线程、锁、runloop、

    KVO & KVC KVO用法和底层原理 使用方法:添加观察者,然后怎样实现监听的代理 KVO底层使用了 isa-...

  • KVO

    KVO (Key-value-observing) 键值监听 iOS用什么方式实现对一个对象的KVO?(KVO的本...

  • iOS底层原理汇 - 探索KVO本质

    问题 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 如何手动触发KVO ? 首先需要了解KVO...

  • KVO的本质

    面试问题: iOS用什么方式实现对一个对象的KVO? 如何手动触发KVO? KVO简介 KVO就是键值观测。有时候...

  • IOS基础知识-KVO原理篇

    问题 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)如何手动触发KVO KVO的全称 Key-V...

  • KVO

    iOS用什么方式实现对一个对象的KVO 如何手动触发KVO 直接修改成员变量会触发KVO么? KVO [self....

  • KVO和KVC的本质

    一、KVO 问题 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?) 如何手动触发KVO? 1. K...

网友评论

    本文标题:自己实现KVO(代理方式)

    本文链接:https://www.haomeiwen.com/subject/ufofpltx.html