自定义KVO

首先了解一下系统的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 

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 *)change context:(void *)context {
    
    NSLog(@"+++%@",self.s.age);
}

-(void)touchesBegan:(NSSet *)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)