iOS之封装集成

手动实现带有block的KVO

2017-04-27  本文已影响15人  LQWAWAIOS

上篇文章讲到了什么是isa指针以及KVO的底层实现,如果对KVO和isa指针不熟悉的需要先看看这篇文章。本篇文章主要是实现含有Block的KVO方法。先上代码

1、 KVO的简单实现

上篇文章中我们知道KVO的底层是通过运行时动态创建一个子类进行监听属性的变化的。我们这里先给出一个简单的实现:
1、创建一个Dog类,含有有个name属性。
2、手动创建一个SimpleKVO_Dog类继承自Dog类,重写setName方法。
3、给NSObject添加个category,增加添加观察者的方法和观察者回调方法,实现代码如下:
NSObject的category中的代码:

#import "SimpleKVO_Dog.h"
NSString *const ObserverKey = @"ObserverKey";
@implementation NSObject (SimpleKVO)
- (void)ll_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context{
    // 保存观察者
    objc_setAssociatedObject(self, (__bridge const void *)(ObserverKey), observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    // 修改isa指针指向的类(指向了Dog子类),这里将指向我们手动创建的Dog的子类
    object_setClass(self, [SimpleKVO_Dog class]);
}

// 这里做是为了容错处理
- (void)ll_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context{}
@end

子类重写的setName方法实现:

- (void)setName:(NSString *)name{
    // 保存旧值
    NSString *oldName = self.name;
    // 调用父类方法
    [super setName:name];
    // 获取观察者
    id obsetver = objc_getAssociatedObject(self, ObserverKey);
    NSDictionary<NSKeyValueChangeKey,id> *changeDict = oldName ? @{NSKeyValueChangeNewKey : name, NSKeyValueChangeOldKey : oldName} : @{NSKeyValueChangeNewKey : name};
    // 调用回调方法,传递旧值和新值
    [obsetver ll_observeValueForKeyPath:@"name" ofObject:self change:changeDict context:nil];
}

调用代码:

- (void)test{
    Dog *dog =  [Dog new];
   //isa --->Dog类
    [dog ll_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew |
     NSKeyValueObservingOptionOld context:nil];
  //isa---> SimpleKVO_Dog类
    dog.name = @"aaa";
    dog.name = @"bbb";
}
- (void)ll_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

打印结果:


上述方法中我们是通过手动创建Dog的子类SimpleKVO_Dog类,并重写了父类的setName
方法,通过修改Dog类实例isa指针的指向,来调用子类的setName方法。子类的setName方法中又调用父类的setName方法以及通知了观察者属性改变。

2、带有block的实现

下面我们来通过runtime动态生成子类,并实现带有block回调的方法,我们仍以Dog类为例。下面我们通过代码进一步去讲解:
首先先给出两个工具方法(getter方法名和setter方法名的相互转化):

//根据getter方法名返回setter方法名   name -> Name -> setName:
- (NSString *)setterForGetter:(NSString *)key
{
    // 1. 首字母转换成大写
    unichar c = [key characterAtIndex:0];
    NSString *str = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c-32]];
    // 2. 最前增加set, 最后增加:
    NSString *setter = [NSString stringWithFormat:@"set%@:", str];
    return setter;
}

//根据setter方法名返回getter方法名  setName: -> Name -> name
- (NSString *)getterForSetter:(NSString *)key
{
    // 1. 去掉set
    NSRange range = [key rangeOfString:@"set"];
    NSString *subStr1 = [key substringFromIndex:range.location + range.length];
    // 2. 首字母转换成大写
    unichar c = [subStr1 characterAtIndex:0];
    NSString *subStr2 = [subStr1 stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[NSString stringWithFormat:@"%c", c+32]];
    // 3. 去掉最后的:
    NSRange range2 = [subStr2 rangeOfString:@":"];
    NSString *getter = [subStr2 substringToIndex:range2.location];
    return getter;
}

下面我们来看具体逻辑实现:

添加观察者:ll_addObserver:key:callback:步骤:
-(void)ll_addObserver:(id)observer key:(NSString *)key callback:(LLKVOBlock)callback{
    //1. 通过观察的key获得相应的setter方法
    SEL setterSelector = NSSelectorFromString([self setterForGetter:key]);
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod)  return;    //不存在setter方法直接return
    
    //2. 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类
    Class clazz = object_getClass(self);
    NSString *className = NSStringFromClass(clazz);
    if (![className hasPrefix:KVOPrefix]) {//当前类不是KVO类
        clazz = [self ll_KVOClassWithOriginalClassName:className];
        object_setClass(self, clazz);
    }

    //-------到这里self已经是KVO类了---------
    
    // 3. 检查KVO类是否已重写父类的setter方法,如果没有则为KVO类添加setter方法的实现
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
    };
    
    // 4. 添加该观察者到观察者列表中
    // 4.1 创建观察者相关信息字典(观察者对象、观察的key、block)
    NSDictionary *infoDic = @{@"observer":observer,@"key":key,@"callback":callback};
    // 4.2 获取关联对象(装着所有观察者的数组)
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, ObserverArrayKey, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    [observers addObject:infoDic];
}
创建子类的步骤:
//  动态创建子类的方法
-(Class)ll_KVOClassWithOriginalClassName:(NSString *)className
{
    NSString *kvoClassName = [KVOPrefix stringByAppendingString:className];
    Class kvoClass = NSClassFromString(kvoClassName);//如果类不存在这个方法返回的值为nil
    // 如果kvo class存在则返回
    if (kvoClass) {
        return kvoClass;
    }
    // 如果kvo class不存在, 则创建这个类
    Class originClass = object_getClass(self);
    kvoClass = objc_allocateClassPair(originClass, kvoClassName.UTF8String, 0);
    
    // 修改kvo class方法的实现
    Method clazzMethod = class_getInstanceMethod(kvoClass, @selector(class));
    const char *types = method_getTypeEncoding(clazzMethod);
    class_addMethod(kvoClass, @selector(class), (IMP)ll_class, types);
     // 注册kvo_class
    objc_registerClassPair(kvoClass);
    
    return kvoClass;
    
}
// 重写的class方法的IMP
static Class ll_class(id self, SEL cmd)
{
    //模仿Apple的做法, 欺骗人们这个kvo类还是原类
   return  class_getSuperclass(object_getClass(self));
}

下面我们来看一下子类的setter方法的实现,这和简单实现的思路是一样的,同样是:

static void kvo_setter(id self, SEL _cmd, id newValue)
{
    
    // 1.  获取旧值
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = [self getterForSetter:setterName];
    id oldValue = [self valueForKey:getterName];
    
    // 2. 调用父类方法
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    objc_msgSendSuper(&superClazz, _cmd, newValue);
    
    // 3、获取观察者列表,遍历找出对应的观察者,执行响应的block
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    for (NSDictionary *info in observers) {
        if ([info[@"key"] isEqualToString:getterName]) {
            // gcd异步调用callback
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                ((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, newValue);
            });
        }
    }
}

注:这里调用父类的方法通过id objc_msgSendSuper(struct objc_super *super, SEL op, ...)实现,第一个参数是一个指向objc_super结构体的指针, objc_super的定义如下

struct objc_super {  
    __unsafe_unretained id receiver;    
    __unsafe_unretained Class super_class;  
};  

objc_super结构体包含两个成员,receiver表示某个类的实例,这里为self,super_class表示当前类的父类,这里为self的父类。(注:这里的self其实已经是KVO创建的子类类型了)我们这里通过class_getSuperclass(object_getClass(self))方法获得;
到这里添加观察者的方法暂时差不多了,为什么说暂时呢因为还有些问题,在下面会提出。那么现在我们还需要添加移除观察者的方法:

-(void)ll_removeObserver:(id)observer key:(NSString *)key
{
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    if (!observers) return;

    for (NSDictionary *info in observers) {
        if([info[@"key"] isEqualToString:key]) {
            [observers removeObject:info];
            break;
        }
    }
    // 如果观察者列表count为0,则修改kvo类的isa指针,指向原来的类
    if (observers.count == 0) {
        Class clazz = object_getClass(self);
        NSString *className = NSStringFromClass(clazz);
        Class oriClass =NSClassFromString([className substringFromIndex:KVOPrefix.length]);
        object_setClass(self, oriClass);
    }
}

值得注意的是,当所有观察者都移除后,修改isa指针使其指向原来的类。系统的KVO实现就是这么做的,我们可以简单的通过代码测试一下:

-(void)test1{
    self.dog =  [Dog new];
    NSLog(@"%@",object_getClass(self.dog));
    [self.dog addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew |
     NSKeyValueObservingOptionOld context:nil];
    NSLog(@"%@",object_getClass(self.dog));
    [self.dog removeObserver:self forKeyPath:@"age"];
    NSLog(@"%@",object_getClass(self.dog));
}

输出结果:Dog----NSKVONotifying_Dog----Dog


3、 存在的问题:

因为重写setter方法我们的实现static void kvo_setter(id self, SEL _cmd, id newValue){}是 这样的,newValue是一个id类型,这就要求我们观察的属性必须是OC类的实例。通过尝试发现系统的KVO会将基本类型最终转换成NSNumber类型,再将新/旧值通过字典传递。但是OC对象我们可以通过id来统一表示,基本类型我们却无能为力。所以这里给出两种思路:

// 3. 检查KVO类是否已重写父类的setter方法,如果没有则为KVO类添加setter方法的实现
    if (![self hasSelector:setterSelector]) {
        const char *types = method_getTypeEncoding(setterMethod);
        // 获取参数类型
        char *type = method_copyArgumentType(setterMethod, 2);
        if (strcmp(type, "@") == 0) {//对象类型
            class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
        }else if (strcmp(type, @encode(long))  == 0) {
            class_addMethod(clazz, setterSelector, (IMP)long_setter, types);
        }else if (strcmp(type, @encode(int)) == 0) {
            class_addMethod(clazz, setterSelector, (IMP)int_setter, types);
        }else if (strcmp(type, @encode(float)) == 0) {
            class_addMethod(clazz, setterSelector, (IMP)double_setter, types);
        }else if (strcmp(type, @encode(double))  == 0) {
            class_addMethod(clazz, setterSelector, (IMP)double_setter, types);
        }else if (strcmp(type, @encode(BOOL)) == 0) {
           class_addMethod(clazz, setterSelector, (IMP)bool_setter, types);
        }
    };

但是这种思路的问题就是需要判断的类型太多,除对象类型外都需要实现不同的setter方法的IMP,而且代码内容大致相同,造成代码的重复。这里给出int类型的setter的方法IMP:

//int 类型
static void int_setter(id self, SEL _cmd, int newValue)
{
    
    // 1. 检查getter方法是否存在
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = [self getterForSetter:setterName];
    if (!getterName) {
        
        return;
    }
    
    // 2. 获取旧值
    id oldValue = [self valueForKey:getterName];
    
    // 3. 调用父类方法
    struct objc_super superClazz = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };
    objc_msgSendSuper(&superClazz, _cmd, newValue);
    
    // 4、获取观察者列表,遍历找出对应的观察者,执行响应的block
    NSMutableArray *observers = objc_getAssociatedObject(self, ObserverArrayKey);
    for (NSDictionary *info in observers) {
        if ([info[@"key"] isEqualToString:getterName]) {
            // gcd异步调用callback
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                ((LLKVOBlock)info[@"callback"])(info[@"observer"], getterName, oldValue, [NSNumber numberWithInt:newValue]);
            });
        }
    }
}

到这里本篇文章基本结束,文中所涉及代码都在这里
最后:喜欢我文章的可以多多点赞和关注,您的鼓励是我写作的动力。O(∩_∩)O~


参考

上一篇下一篇

猜你喜欢

热点阅读