美文网首页
自定义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);
    }];

相关文章

  • iOS runtime自定义实现KVO

    1、了解KVO 打印结果: 2、自定义实现KVO .h .m

  • KVO基本使用

    分三部分解释KVO一.KVO基本使用二.KVO原理解析三.自定义实现KVO 一、KVO基本使用 使用KVO,能够非...

  • KVC/ KVO

    1、kvc原理: 45页 2、自定义KVO KVO参考链接 KVO默认观察setter,使用isa-swizzli...

  • iOS-底层原理-自定义KVO

    1.自定义KVO 1.上一篇博客了解了iOS 系统KVO的底层实现原理,那么这里进行自定义KVO,更好的理解原理和...

  • iOS 自定义KVO

    利用Runtime 实现简单的自定义kvo 代码githubgithub.com/zswj/custom-KVO ...

  • 自定义KVO,自动释放,监听多个属性

    自定义的 KVO,支持多属性监听,支持自动释放。 使用系统 KVO 监听属性 先来回顾下系统 KVO 是如何使用的...

  • 21.iOS底层学习之KVO自定义

    本篇文章提纲:1、自定义KVO2、函数式KVO3、KVO的自动销毁机制4、FBKVOController5、GNU...

  • KVO 本质 & 自定义实现

    KVO 本质 & 自定义实现 KVO 是什么? Key-Value Observer 即键值观察者。作用为监听某个...

  • Key-Value Observing(kvo)二:自定义kvo

    一、自定义kvo 在上篇文章 kvo原理分析[https://www.jianshu.com/u/a569f590...

  • iOS - 自定义KVO

    之前我们已经了解过了KVO的底层实现原理,不过呢,在我们开始实现自定义KVO之前再来简单回顾下KVO的实现原理 1...

网友评论

      本文标题:自定义KVO

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