美文网首页
自定义KVO

自定义KVO

作者: melody5 | 来源:发表于2018-09-20 16:06 被阅读6次

    首先了解一下系统的KVO实现原理:其实就是动态的创建了一个被观察者的子类,然后动态修改它的isa指针指向它的子类,在子类里重写属性的set方法,最后在set方法里监听属性变化,并发出通知。

    1、验证系统原理:

    image.png
    image.png
    打个断点,发现实例s的isa是指向Student的,然后单步执行一下,
    image.png
    image.png
    这时候发现实例s的isa指针变成了NSKVONotifying_Student,由此可见,当我们添加观察者的时候,系统动态的创建了一个子类NSKVONotifying_Student,并把s的类型修改成了它。

    2、接下来我们自己用RunTime来仿照系统KVO原理来自己写一个KVO

    首先创建一个NSObject的分类NSObject+KVO,然后自己实现一个监听方法。

    #import "NSObject+KVO.h"
    #import <objc/message.h>
    
    static const char* SJKVOAssiociateKey = "SJKVOAssiociateKey";
    
    @implementation NSObject (KVO)
    -(void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
        
        Class newClass = [self createClass:keyPath];
    
        object_setClass(self, newClass);
        
        // 4.将观察者与对象绑定
        objc_setAssociatedObject(self, SJKVOAssiociateKey, observer, OBJC_ASSOCIATION_ASSIGN);
        
    }
    - (Class) createClass:(NSString*) keyPath {
        
        // 1. 拼接子类名 / SJKVO_Student
        NSString* oldName = NSStringFromClass([self class]);
        NSString* newName = [NSString stringWithFormat:@"SJKVO_%@", oldName];
        
        // 2. 创建并注册类
        Class newClass = NSClassFromString(newName);
        if (!newClass) {
            
            // 创建并注册类
            newClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
            objc_registerClassPair(newClass);
            
            // 动态添加方法
            // class
            Method classMethod = class_getInstanceMethod([self class], @selector(class));
            const char* classTypes = method_getTypeEncoding(classMethod);
            class_addMethod(newClass, @selector(class), (IMP)SJ_class, classTypes);
    
        }
        
        // setter
        NSString* setterMethodName = getSetter(keyPath);
        SEL setterSEL = NSSelectorFromString(setterMethodName);
        Method setterMethod = class_getInstanceMethod([self class], setterSEL);
        const char* setterTypes = method_getTypeEncoding(setterMethod);
        
        class_addMethod(newClass, setterSEL, (IMP)SJ_setKey, setterTypes);
    
        return newClass;
    }
    
    Class SJ_class(id self, SEL _cmd) {
        return class_getSuperclass(object_getClass(self));
    }
    
    void SJ_setKey(id self, SEL _cmd, id newValue) {
        struct objc_super oldSuper = {self,class_getSuperclass([self class])};
        // 修改属性值
        objc_msgSendSuper(&oldSuper, _cmd, newValue);
        // 拿出观察者
        id observer = objc_getAssociatedObject(self, SJKVOAssiociateKey);
        NSLog(@"---%@",newValue);
        
        // 调用observer
        NSString *methodName = NSStringFromSelector(_cmd);
        NSString *key = getValueKey(methodName);
        
        objc_msgSend(observer, sel_registerName("observeValueForKeyPath:ofObject:change:context:"),key,self,@{key:newValue},nil);
        
    //    [observer observeValueForKeyPath:key ofObject:self change:@{key:newValue} context:nil];
    }
    // key -> setter
    static NSString  * getSetter(NSString *keyPath){
        
        if (keyPath.length <= 0) { return nil; }
        
        NSString *firstString = [[keyPath substringToIndex:1] uppercaseString];
        NSString *leaveString = [keyPath substringFromIndex:1];
        
        return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
    }
    
    // cmd -> key
    NSString* getKey(NSString * cmd) {
        if (cmd.length <= 0 || ![cmd hasPrefix:@"set"] || ![cmd hasSuffix:@":"]) { return nil;}
        
        NSRange range = NSMakeRange(3, cmd.length-4);
        NSString *getter = [cmd substringWithRange:range];
        NSString *firstString = [[getter substringToIndex:1] lowercaseString];
        getter = [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
        
        return getter;
        
    }
    
    @end
    

    然后在用我们自己的方法来添加一下观察者,看是否能观察到

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        Student *s = [[Student alloc] init];
        
    //    [s addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
        
        [s SJ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
        
        self.s = s;
    }
    
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        
        NSLog(@"+++%@",self.s.age);
    }
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        static NSInteger i = 0;
        i++;
        self.s.age = [NSString stringWithFormat:@"%ld",i];
    }
    
    image.png

    通过打印可以看出跟系统的效果一样。

    基本都有注释,我就不详解了,里边一些关于runtime的动态函数和消息转发函数,在我之前关于runtime的文章里都有详解,不了解可以参照前几篇文章做基础。

    3、用block回调

    用系统的回调会一个问题,就是如果观察的属性多了,在回调方法里需要先判断是哪个对象的哪个属性,比较麻烦,但是用block回调的话,就省去了这些麻烦,并且代码逻辑更清晰,更紧凑。接下来代码多了,我们顺便整理封装一下。

    首先定义一个block和一个可以有block的构造方法:

    typedef void(^ValueChangeBlock)(id observer, NSString* keyPath, id oldValue, id newValue);
    
    @interface NSObject (KVO)
    // 系统回调
    - (void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    // block回调
    - (void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context ValueChangeBlock:(ValueChangeBlock)valueChangeBlock;
    
    @end
    

    然后在.m文件内部创建一个类,来存储要监听的信息:

    static const char* SJKVOAssiociateKey = "SJKVOAssiociateKey";
    
    @interface SJInfo : NSObject
    
    @property (nonatomic, weak) NSObject* observer;
    @property (nonatomic, strong) NSString* keyPath;
    @property (nonatomic, copy) ValueChangeBlock valueChangeBlock;
    
    @end
    
    @implementation SJInfo
    
    - (instancetype) initWithObserver:(NSObject*)observer forKeyPath:(NSString*) keyPath valueChangeBlock:(ValueChangeBlock) block {
        if (self == [super init]) {
            _observer = observer;
            _keyPath = keyPath;
            _valueChangeBlock = block;
        }
        return self;
    }
    
    @end
    

    然后在创建是就把参数和block保存到数组里,以便下边拿出来调用:

    -(void)SJ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context ValueChangeBlock:(ValueChangeBlock)valueChangeBlock {
    
        Class newClass = [self createClass:keyPath];
    
        object_setClass(self, newClass);
    
        // 信息保存
        SJInfo* info = [[SJInfo alloc] initWithObserver:observer forKeyPath:keyPath valueChangeBlock:valueChangeBlock];
        NSMutableArray* array = objc_getAssociatedObject(self, SJKVOAssiociateKey);
        if (!array) {
            array = [NSMutableArray array];
            objc_setAssociatedObject(self, SJKVOAssiociateKey, array, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        [array addObject:info];
    
    }
    
    void SJ_setKey(id self, SEL _cmd, id newValue) {
        
        struct objc_super oldSuper = {self,class_getSuperclass(object_getClass(self))};
        
        // 获取key
        NSString *key = getKey(NSStringFromSelector(_cmd));
        
        // 获取旧值
        id oldValue = objc_msgSendSuper(&oldSuper, NSSelectorFromString(key));
        
        // 修改属性值
        objc_msgSendSuper(&oldSuper, _cmd, newValue);
        
        NSMutableArray* array = objc_getAssociatedObject(self, SJKVOAssiociateKey);
        if (array) {
            for (SJInfo* info in array) {
                if ([info.keyPath isEqualToString:key]) {
                    info.valueChangeBlock(info.observer, key, oldValue, newValue);
                    return;
                }
            }
        }
    }
    

    在setKey方法里拿出数组中的信息,找到对应的key,然后block回调就可以了,这样就可以同时监听多个属性了。

    销毁观察者

    其实销毁观察者就是把isa指针指从子类回到原来就可以了,我们把他放在dealloc方法里来做比较合适,有两种方法:1.利用hook在创建子类的方法里做方法交换,把dealloc的方法实现指向自己的方法里,然后做isa指回;2.也是在创建子类的时候同时动态添加自己的dealloc方法来做,我们这里就用第二种实现一下:

    放在子类的创建方法里,保证只创建一次

            // 添加SJ_Dealloc,销毁观察者
            SEL deallocSEL = NSSelectorFromString(@"dealloc");
            Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
            const char* deallocTypes = method_getTypeEncoding(deallocMethod);
            class_addMethod(newClass, deallocSEL, (IMP)SJ_Dealloc, deallocTypes);
    
    void SJ_Dealloc(id self, SEL _cmd) {
        // 父类
        Class superClass = [self class];//class_getSuperclass(object_getClass(self));
        
        object_setClass(self, superClass);
    
    }
    

    最后在外边调用一下,非常方便

        [s SJ_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil ValueChangeBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
            NSLog(@"oldValue ---- %@, newValue ---- %@", oldValue, newValue);
        }];
        
        [s SJ_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil ValueChangeBlock:^(id observer, NSString *keyPath, id oldValue, id newValue) {
            NSLog(@"oldValue ++++ %@, newValue ++++ %@", oldValue, newValue);
        }];
    

    相关文章

      网友评论

          本文标题:自定义KVO

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