底层原理-20-kvo的自定义|8月更文挑战

上一篇我们分析了kvo的原理,那么我们是否可以自定义一个自己的kvo呢?主要大致思路

  1. 添加观察者,创建中间类,isa替换。
  2. 中间类setter实现包括回调,父类setter方法调用。
  3. 移除操作,isa还原

1.中间类创建

我们基于NSObject创建自定义KVO分类

@interface NSObject (KBKVO)

//添加

- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
//观察回调

-(void)kb_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
//移除

- (void)kb_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end
复制代码

1.1 setter方法的判断

我们的kvo是基于对setter方法的监听,所以我们添加观察者,先要确保keypath否实现setter方法

 [self judgeMethodIsExist:keyPath];//setter方法是否实现
-(void)judgeMethodIsExist:(NSString*)keyPath

{

    Class class = object_getClass(self);

    NSString *setKey = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];

    SEL sel = NSSelectorFromString(setKey);

    Method method = class_getInstanceMethod(class, sel);

    if (!method) {

        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"没实现%@的setter方法,无法使用kvo",keyPath] userInfo:nil];

    }

}
复制代码

运行一下,我们先观察KBPerson的成员变量nickName,没有实现setter方法,进入报错逻辑

image.png

1.2 中间类实现

-(Class)createSubClassWithKeyPath:(NSString*)keyPath

{

    NSString *superClassName = NSStringFromClass(self.class);

    NSString *associateClassName = [NSString stringWithFormat:@"%@%@",kKBKVOProfix,superClassName];

    Class associateClass = NSClassFromString(associateClassName);

    if (associateClass) {

        return  associateClass;//1.存在就直接返回不用在创建了

    }

    associateClass = objc_allocateClassPair(self.class, associateClassName.UTF8String, 0);//2.1申请类

    

    objc_registerClassPair(associateClass);//2.2注册

    // 2.3.1 : 添加class : class的指向是KBPerson

    SEL classSEL = NSSelectorFromString(@"class");

    Method classMethod = class_getInstanceMethod([self class], classSEL);

    const char *classTypes = method_getTypeEncoding(classMethod);

    class_addMethod(associateClass, classSEL, (IMP)kb_class, classTypes);
    
    // 2.3.2 setter方法添加

    NSString *setKey = [NSString stringWithFormat:@"set%@:",keyPath.capitalizedString];

    SEL sel = NSSelectorFromString(setKey);//方法名

    

    Method method = class_getInstanceMethod(self.class, sel);

    const char *type = method_getTypeEncoding(method);//方法类型

    

    class_addMethod(associateClass,sel, (IMP)kb_setter, type);//添加方法

    

    

    return associateClass;

    

    

}
Class kb_class(id self,SEL _cmd){

    return class_getSuperclass(object_getClass(self));

}
static void kb_setter(id self,SEL _cmd,id newValue){

    

    NSLog(@"自定义来了:%@",newValue);

}
复制代码
  1. 我们先判断是否已经存在,不存在再去创建。
  2. 申请关联类,之后进行注册关联类。
  3. 给关联类添加setter方法,并实现它。

1.3 中间类指向实例

- (void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{

    

    //1.判读setter方法实现

    [self judgeMethodIsExist:keyPath];

    

    //2. 实现中间类

    Class newClass = [self createSubClassWithKeyPath:keyPath];

    

    

    //3. isa指向关联类

    

    object_setClass(self, newClass);

       

}
复制代码

我们观察属性name,进入了中间类的setter方法。

image.png

2. setter方法实现

2.1 正常逻辑

按照理解我们会发通知告诉系统改变了属性值,同时调用父类的setter方法,打印如下:
image.png
题外:调用系统进行消息发送的时候会报错 Too many arguments to function call, expected 0, have 3我们按图修改下。
image.png
也可以 objc_msgSendSuper函数是无返回参数的函数,需要把它强制转化类型

void (*kb_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;

//    objc_msgSendSuper(&superStruct,_cmd,newValue);

    kb_msgSendSuper(&superStruct,_cmd,newValue);
复制代码

之后进行通知观察者回调,我们就需要对观察者发送消息。首先保存观察者

NSObject *oldObserver =  objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));

    if (!oldObserver) {

        

        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }
复制代码

之后进行调用,但是又要记录值的变化,,记录keypath 传过去很麻烦。

SEL observerSEL = @selector(kb_observeValueForKeyPath:ofObject:change:context:);

objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
复制代码

2.2 函数式编程处理

我们日常iOS开发中是面向对象开发,基于mvc的架构模式,有的会进行mvvmmvp模式降低耦合性。但是有的时候模块之间还是有关联性,难以解耦合,使用函数式编程就可以解决部分耦合问题。
比如:函数f=x(),我们可以把整个函数作为一个参数进行传递。f = x(x())。在iOS中我们通常把block作为参数也是一种体现。关于函数式思想,以及链式语法有机会写一篇探讨下。

  1. 我们直接把通知回调写在添加方法中,这样可以是我们的逻辑更加紧密,RAC框架对这样block回调封装了很多系统的功能,比如监测textField的变化直接block回调button点击block回调等。
typedef void(^KBKVOBlock)(NSObject* observer,NSString*keyPath,id oldValue,id newValue);
-(void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KBKVOBlock)block;
复制代码

首先我们需要保存observerkeypathoption,我们创建一个类专门保存这些信息

//仿照系统

@interface KBKVOInfo : NSObject

@property (nonatomic, weak) NSObject  *observer;//防止循环引用

@property (nonatomic, copy) NSString    *keyPath;

@property (nonatomic, copy) KBKVOBlock handleBlock;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handBlock:(KBKVOBlock)block;

@end
@implementation KBKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handBlock:(KBKVOBlock)block{

    if (self=[super init]) {

        _observer = observer;

        _keyPath  = keyPath;

        _handleBlock = block;

    }

    return self;

}
@end
复制代码

我们在添加观察者的时候把observerkeyPathblock放在info中并保存info

-(void)kb_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(KBKVOBlock)block

{
****************************************关联*******************
KBKVOInfo *info = [[KBKVOInfo alloc]initWitObserver:observer forKeyPath:keyPath handBlock:block];

    //4.2保存,考虑复用性,我们把info放到一个数组进行关联

    NSMutableArray *arr  = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));

    if (!arr) {

        arr = [NSMutableArray arrayWithCapacity:1];//第一次

        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), arr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    

    }

    [arr addObject:info];
  }
复制代码

具体实现kb_setter

static void kb_setter(id self,SEL _cmd,id newValue){

    //1.计算旧值

    NSString *keyPath =  getterForSetter( NSStringFromSelector(_cmd));

    id oldValue = [self valueForKey:keyPath];

    

    //1.调用父类的方法

    Class superClass = class_getSuperclass(object_getClass(self));

    struct objc_super superStruct = {

    .receiver = self,

    .super_class = superClass,

    };

    void (*kb_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;

//    objc_msgSendSuper(&superStruct,_cmd,newValue);

    kb_msgSendSuper(&superStruct,_cmd,newValue);

    NSLog(@"自定义来了:%@",newValue);

    //2.告诉系统将要改变

   
    NSMutableArray *arr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));

    for (KBKVOInfo *info in arr) {

        if (info.handleBlock && [info.keyPath isEqualToString:keyPath]) {
            

            info.handleBlock(info.observer, keyPath, oldValue, newValue);

        }

    }

}
复制代码

取出关联数组,根据keyPath找到info对象,调用回调

image.png

3. 移除观察者,还原isa

三部曲最后一步,移除。我们知道removeObserver主要做了将isa还原。由于我们自定义关联对象的数组中存在保存的KBKVOInfo信息,因此要进行移除操作。

- (void)kb_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath

{

    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey));

    if (observerArr.count<=0) {

        return;

    }

    

    for (KBKVOInfo *info in observerArr) {

        if ([info.keyPath isEqualToString:keyPath]) {

            [observerArr removeObject:info];

            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKBKVOAssociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

            break;

        }

    }

    

    if (observerArr.count<=0) {

        // 指回给父类

        Class superClass = [self class];

        object_setClass(self, superClass);

    }
  

}
//调用
-(void)dealloc

{

    [self.person kb_removeObserver:self forKeyPath:@"name"];

}
复制代码

4. 自动移除

由于我们要每次dealloc中手动去移除操作,那么是否能有一种自动的方式呢?
我们监听子类的dealloc方法,当被观察者调用dealloc的时候,我们去调用移除操作
之前我们了解过methd-swizzing方法交换,我们可以把子类的dealloc进行交换。

+(void)load

{
//交换
    [self kb_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(customeDealloc)];

    

}

-(void)customeDealloc

{

   //还原isa
    Class superClass = [self class];

    object_setClass(self, superClass);

}
//方法交换,具体可了解methd-swizzing
+ (BOOL)kb_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {

    Class cls = self;

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);

    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);

    

    if (!swiMethod) {

        return NO;

    }

    if (!oriMethod) {

        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));

    }

    

    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

    if (didAddMethod) {

        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));

    }else{

        method_exchangeImplementations(oriMethod, swiMethod);

    }

    return YES;

}
复制代码

这样写有2个问题

  1. 我们分类重写会导致所有的类都交换了。
  2. +load方法里面的代码不一定只走一次
+(void)load

{

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        //只执行一次,防止交换2的倍数,交换回来了。

        [self kb_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(customeDealloc)];

    });

    

}

-(void)customeDealloc

{
    Class superClass = [self class];

    object_setClass(self, superClass);

    [self customeDealloc];//交换后imp,再次调用实现原有的dealloc方法。

}
复制代码

修改后:1. 交换后imp,再次调用实现原有的dealloc方法,不影响原有方法调用。
2. 防止主动调用,使用单例的形式只执行一次,防止交换2的倍数,交换回来了。

5. 总结

kvo的自定义,大致分为3个步骤

  1. 中间类的创建setter方法是否实现, 中间类的实现,isa指向中间类)
  2. setter方法实现(通过block的回调,进行处理)
  3. 移除(方法交换,实现自动移除,还原isa

过程中我们学习函数式编程的思想,当然有些不是完善的地方,比如自动移除的时候没有移除关联数组中的保存的信息。Facebook实现了git上有关于kvo的自定义实现,感兴趣的可以看下 KVOController

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享