KVO 探究(一)自己实现KVO

2019-05-15  本文已影响0人  木兮_君兮

何为KVO

KVO 是 key value observing 的简写:
苹果官方文档的解释:Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

KVO的意义

KVO 是应对观察者模式而产生的,当被观察者发生相应的改变,观察者对象会得到一个通知。

KVO 是如何实现的

KVO 运用的是runtime来进行处理的。当我们观察一个对象的时候,一个新类会被动态的创建。这个新类继承自原来的对象的类。继承之后苹果重写了被观察属性的setter方法。苹果会在调用原来的setter方法的时候通知所观察对象发生了改变。然后将这个对象的isa指向了新创建的类,这个对象就变成了新创建的子类的对象。除此之外,苹果还偷天换日重写了-class方法,让我们觉得这个类并没有改变。

苹果的描述

Automatic key-value observing is implemented using a techique called isa-swizzling ... when an observer is registerd for an atttribute of object the isa pointer of the observed object is modified,pointing to an intermediate class rather than at the true class...

KVO的问题

  1. -addObserver:forKeyPath:options:context (dosen't allow passing a custom selector to be invoked。---- kvo不允许我们添加block 和 selector。暴露出来的api 比较鸡肋。
  2. 我们纵观NSNotificationCenter 我们就能了解KVO 是可以进行方法的指定的;

KVO的用法

给某个对象的某个属性(可以是基本数据类型,也可以是对象类型)添加一个,观察者,我们观察的属性行为有啥(new ,old ,即为NSKeyValueChangeKey)。
//注册:
[self addObserver:self forKeyPath:@"tempt" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
//对应要取消 ,在注册类的析构函数里面进行取消
removeObserver: forKeyPath:
//观察方法
observeValueForKeyPath:ofObject:object change:change context:

问题

我们观察属性一般发生了变化就要在观察方法中进行相应的业务逻辑的处理,我们将业务逻辑放到下面是不是不太聚合业务。我们有没有什么办法,在注册的时候直接就可以看到业务逻辑,而不用还要往下翻去找这个函数再看业务逻辑呢?当然有,聚合在一起我们首先就会想到一个神奇的东西,代码块,在iOS中即为Block。

如何手动实现KVO

思路:KVO实现的原理是,当我们监听一个对象的某个属性的时候,就会产生一个类的子类,然后对这个类被监听的属性进行setter方法的重写(要考虑这个这个方法有没有setter方法)。然后我们再将对象的isa指针指向这个子类。最后我们再来一步,偷天换日,重写class 类的方法。 最重要一点 千万要remove observer

具体思路

  1. 看setter方法有没有,没有则返回,抛出异常。(KVO的本质是改写setter方法,在里面添加修改属性的出发方法)
  2. 类是否为KVO类,不是就新建子类,将isa 指向这个类。
  3. KVO 是否重写setter方法,没有就添加
    4.保存信息,留作setter触发方法的时候使用。

所需知识点

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

开始撸

设计接口

@interface NSObject (LBKVO)

typedef void (^LBKVOBlock)(id observer, NSString *key, id oldValue, id newValue);

//接口设计 ,需要什么属性呢? 观察者,被观察者的属性,对应条件下发生的改变的回调。
- (void)LB_addObserver:(NSObject *)observer keyPath:(NSString *)keyPath changeBlock:(LBKVOBlock)changeBlock;

- (void)LB_removeObserver:(NSObject *)observer keyPath:(NSString *)keyPath;

@end

实现

  1. 看setter方法有没有,没有则返回,抛出异常。(KVO的本质是改写setter方法,在里面添加修改属性的出发方法)
#pragma mark - 检查是否有设置方法
- (Method)p_checkIsImplementionSetterWithContext:(NSObject *)context keypath:(NSObject *)keypath{
    //get method
    SEL selector = NSSelectorFromString(setterForGetter(keypath));
    Method setMethod = class_getInstanceMethod([self class], selector);
    return setMethod;
}

  1. 类是否为KVO类,不是就新建子类,将isa 指向这个类。
 //2. 判断这个类是否是自定义KVO类
    Class clazz = object_getClass(self);
    NSString *clazzName = NSStringFromClass(clazz);
    //3. 没有就创建一个类并且让该对象的isa指向
    if (![clazzName hasPrefix:kLBKVOPrefix]) {
        clazz = [self createKVOClassWithClassName:clazzName];
        object_setClass(self, clazz); //指向这个类
    }

2.1 如何创建一个子类呢?

- (Class)createKVOClassWithClassName:(NSString *)className{
    NSString *kvoClazzName = [kLBKVOPrefix stringByAppendingString:className];
    Class KVOClazz = NSClassFromString(kvoClazzName);
    if (KVOClazz) {
        return KVOClazz;
    }
    //创建
    Class superClazz = object_getClass(self);
    Class KVOClazz1 = objc_allocateClassPair(superClazz, kvoClazzName.UTF8String, 0);
 
    objc_registerClassPair(KVOClazz1);
    return KVOClazz1;
}

2.2 那么我们如何进行偷天换日呢?

    //获得Types类型 ,一定要在registerClassPair方法前进行替换
    Method m = class_getInstanceMethod(superClazz, @selector(class));
    const char *types = method_getTypeEncoding(m);
    class_addMethod(KVOClazz1, @selector(class), (IMP)LBKVO_Class, types);
  1. 重写setter方法
  //重写setter方法
 const char * types = method_getTypeEncoding(superSetMethod);
 class_addMethod(object_getClass(self), NSSelectorFromString(setterForGetter(keyPath)), (IMP)KVO_setterIMP, types);

重点来了

如何setter方法如何进行重写呢

static void KVO_setterIMP(id self,SEL _cmd,id newValue){
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
  
    id oldValue = [self valueForKey:getterName];
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    
    void (*objc_msgSendSuperCasted)(void *,SEL ,id) = (void *)objc_msgSendSuper;
    objc_msgSendSuperCasted(&superClazz,_cmd,newValue);
    NSMutableArray<LBKVOInfo *> *observers = objc_getAssociatedObject(self, (__bridge const void *)kLBKVOObserversKey);
    [observers enumerateObjectsUsingBlock:^(LBKVOInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([(NSString *)obj.key isEqualToString:getterName]) {
            obj.changeBlock(self, getterName, oldValue, newValue);
        }
    }];

}

4.进行信息的保存

   NSMutableArray *observers = objc_getAssociatedObject(self,(__bridge const void *) kLBKVOObserversKey);
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge const void *) kLBKVOObserversKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    LBKVOInfo *info = [[LBKVOInfo alloc] initWithObserver:observers key:keyPath changeBlock:changeBlock];
    [observers addObject:info];

组装

将1,2,3,4逻辑进行组装,就是KVO实现的核心逻辑了。

调用

[self LB_addObserver:self keyPath:@"tempt" changeBlock:^(id  _Nonnull observer, NSString * _Nonnull key, id  _Nonnull oldValue, id  _Nonnull newValue) {
         NSLog(@"sadf");
}];

别忘记remove

-(void)dealloc{
    [self LB_removeObserver:self keyPath:@"tempt"];
}

Demo 传送门: https://github.com/LeonLeeboy/LBKVO

上一篇下一篇

猜你喜欢

热点阅读