阅读 237

iOS底层探索之KVO(四)—自定义KVO

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

回顾

上篇博客已经自定义了KVO,但是还没有完善,还有些问题需要解决,这么本篇博客就把自定义KVO进行完善。

1. 观察者信息保存问题

在上一篇的博客中,自定义KVO的简单逻辑是已经实现了,但是这里还是存在一个大的问题,就是如果我们要观察多个属性的时候,以及新值和旧值,都要观察以及传递了context的情况下就无效了。

解决的办法就是,我们需要保存观察者相关的信息,那么就创建一个新类JPKVOInfo保存,代码的实现如下:

typedef NS_OPTIONS(NSUInteger, JPKeyValueObservingOptions) {
    JPKeyValueObservingOptionNew = 0x01,
   JPKeyValueObservingOptionOld = 0x02,
};

@interface JPKVOInfo : NSObject

@property (nonatomic, weak) id observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) JPKeyValueObservingOptions options;
@property (nonatomic, strong) id context;

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JPKeyValueObservingOptions)options context:(nullable void *)context;

@end

@implementation JPKVOInfo

- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(JPKeyValueObservingOptions)options context:(nullable void *)context {
    self = [super init];
    if (self) {
        self.observer = observer;
        self.keyPath  = keyPath;
        self.options  = options;
        self.context = (__bridge id _Nonnull)(context);
    }
    return self;
}

@end
复制代码

注意:

observer的修饰要使用weak,弱引用,防止循环引用问题

那么在jp_addObserver中信息需要保存观察者信息,如下代码:

JPKVOInfo *info = [[JPKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
    NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey));
    
    if (!observerArr) {
        observerArr = [NSMutableArray arrayWithCapacity:1];
        [observerArr addObject:info];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
复制代码

jp_setter方法里面的逻辑修改之后如下代码:

//1.通知观察者,先拿到观察者
    NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kjPKVOAssiociateKey));
    for (JPKVOInfo *info in observerArray) {//循环调用,可能添加多次。
        if ([info.keyPath isEqualToString:keyPath]) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
                //对新旧值进行处理
                if (info.options & jPKeyValueObservingOptionNew) {
                    [change setObject:newValue forKey:NSKeyValueChangeNewKey];
                }
                if (info.options & jPKeyValueObservingOptionOld) {
                    if (oldValue) {
                        [change setObject:oldValue forKey:NSKeyValueChangeOldKey];
                    } else {
                        [change setObject:@"" forKey:NSKeyValueChangeOldKey];
                    }
                }
                [change setObject:@1 forKey:@"kind"];
                //消息发送给观察者
                [info.observer jp_observeValueForKeyPath:keyPath ofObject:self change:change context:(__bridge void * _Nullable)(info.context)];
            });
        }
    }
复制代码
  • 在调用父类之前得先获取旧的值。
  • 取出关联对象数组数据,循环判断调用。
  • jp_observeValueForKeyPath通知观察者。

这个时候观察多个属性以及多次观察就都没问题了。

2. 移除观察者

  • jp_removeObserver
- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    [self jp_removeObserver:observer forKeyPath:keyPath context:NULL];
}

- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context {
    NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey));
    if (observerArray.count <= 0) {
        return;
    }
    
    NSMutableArray *tempArray = [observerArray mutableCopy];
    for (JPKVOInfo *info in tempArray) {
        if ([info.keyPath isEqualToString:keyPath]) {
            if (info.observer) {
                if (info.observer == observer) {
                    if (context != NULL) {
                        if (info.context == context) {
                            [observerArray removeObject:info];
                        }
                    } else {
                        [observerArray removeObject:info];
                    }
                }
            } else {
                if (context != NULL) {
                    if (info.context == context) {
                        [observerArray removeObject:info];
                    }
                } else {
                    [observerArray removeObject:info];
                }
            }
        }
    }
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kJPKVOAssiociateKey), observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //已经全部移除了
    if (observerArray.count <= 0) {
        //isa指回给父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}
复制代码
  • 通过keyPath以及observercontext确定要移除的关联对象数据。
  • 当关联对象中没有数据的时候isa进行指回。

3. 函数式编程KVO

3.1 注册与回调绑定

我们上面已经完成了KVO的自定义了,这和系统的KVO的实现都有一个问题,都需要三步曲。jp_addObserver jp_observeValueForKeyPath都得分开写,代码分散,逻辑代码和业务代码分太开了,我们能不能用函数式编程思想,将他们放在一起呢?那么试着去实现一下。

先为KVO分类添加一个block,并且在addObserver的函数里面添加上这个block,这样注册和回调就可以在一起处理了。

  • 修改注册方法为block实现:
typedef void(^JPKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JPKVOBlock)block {
......此处省略....
    //保存观察者信息-数组
    JPKVOBlockInfo *kvoInfo = [[JPKVOBlockInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
......此处省略.......
}
复制代码

block实现也保存在JPKVOBlockInfo中,这样在回调的时候直接执行block实现就可以了。

  • 回调逻辑:
NSMutableArray *observerArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPBlockKVOAssiociateKey));
for (JPKVOBlockInfo *info in observerArray) {//循环调用,可能添加多次。
    if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        });
    }
}
复制代码

在回调的时候直接将新值与旧值一起返回。

  • 注册调用逻辑:
[self.obj jp_addObserver:self forKeyPath:@"name" block:^(id  _Nonnull observer, NSString * _Nonnull keyPath, id  _Nonnull oldValue, id  _Nonnull newValue) {
    NSLog(@"block: oldValue:%@,newValue:%@",oldValue,newValue);
}];
复制代码

3.2 KVO自动销毁

即使我们已经实现了注册和回调绑定,但是在观察者dealloc的时候仍然需要remove。 那么怎么能自动释放不需要主动调用呢?

removeObserver的过程中主要做了两件事,移除关联对象数组中的数据以及指回isa。关联对象不移除的后果是会继续调用回调,那么在调用的时候判断下observer存不存在,这样来处理是否回调就可以了?核心就在指回isa了。

对dealloc方法进行Hook

+ (void)jp_methodSwizzleWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL isClassMethod:(BOOL)isClassMethod {
    if (!cls) {
        NSLog(@"class is nil");
        return;
    }
    if (!swizzledSEL) {
        NSLog(@"swizzledSEL is nil");
        return;
    }
    //类/元类
    Class swizzleClass = isClassMethod ? object_getClass(cls) : cls;
    Method oriMethod = class_getInstanceMethod(swizzleClass, oriSEL);
    Method swiMethod = class_getInstanceMethod(swizzleClass, swizzledSEL);
    if (!oriMethod) {//原始方法没有实现
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个空实现
        class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        //添加一个空的实现
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
           NSLog(@"imp default null implementation");
        }));
    }
    //自己没有则会添加成功,自己有添加失败
    BOOL success = class_addMethod(swizzleClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    if (success) {//自己没有方法添加一个,添加成功则证明自己没有。
       class_replaceMethod(swizzleClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else { //自己有直接进行交换
       method_exchangeImplementations(oriMethod, swiMethod);
    }
}

+ (void)load {
    [self jp_methodSwizzleWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
}

- (void)jp_dealloc {
   // [self.obj jp_removeObserver:self forKeyPath:@""];
    [self jp_dealloc];
}
复制代码
  • jp_dealloc中调用jp_removeObserver移除观察者。
  • 但是我们发现有个问题是:被观察者和keypath从哪里来?这里相当于是观察者的dealloc中调用。
  • 所以可以通过在注册的时候对观察者添加关联对象,保存被观察者和keyPath:
static NSString *const kJPBlockKVOObserverdAssiociateKey = @"JPKVOObserverdAssiociateKey";

@interface JPKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString  *keyPath;
@end
@implementation JPKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath {
    if (self=[super init]) {
        _observerd = observerd;
        _keyPath  = keyPath;
    }
    return self;
}
@end
- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JPKVOBlock)block {
    ……    
    //保存被观察者信息
    JPKVOObservedInfo *kvoObservedInfo = [[JPKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath];
    NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPBlockKVOObserverdAssiociateKey));
    if (!observerdArray) {
        observerdArray = [NSMutableArray arrayWithCapacity:1];
    }
    [observerdArray addObject:kvoObservedInfo];
    objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPBlockKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
复制代码
  • 实例化的kvoObservedInfo中保存的是self,也就是被观察者。
  • 关联对象关联在observer也就是观察者身上。

那么现在在dealloc中遍历对其进行移除操作:

- (void)jp_dealloc {
    NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kJPBlockKVOObserverdAssiociateKey));
    for (JPKVOObservedInfo *info in observerdArray) {
        if (info.observerd) {
            [info.observerd jp_removeObserver:self forKeyPath:info.keyPath];
        }
    }
    [self jp_dealloc];
}
复制代码

我们这里的方法的执行,只是针对被观察者没有释放的情况,如果释放了observerd就不存在的情况下,我们是不需要调用remove处理的。

Hook的优化

在上面的做法中,有不合理的问题存在:就是在+ loadHook dealloc方法是在NSObject分类中处理的,那么意味着所有的类的dealloc方法都被Hook了。

那么我们如何改进优化下Hook呢?

解决办法就是:只针对类进行Hook dealloc方法,所以可以将Hook延迟到addObserver中:

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(JPKVOBlock)block {
  ……
 //hook dealloc
 [[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
}
复制代码

但是对dealloc hook我们只能够hook一次,否则又交换回来了。 所以要么做标记,要么在创建kvo子类的时候进行hook。显然在创建子类的时候更合适。代码如下所示:

//申请类-注册类-添加方法
- (Class)_creatKVOClassWithKeyPath:(NSString *)keyPath observer:(NSObject *)observer {
    //这里重写class后kvo子类也返回的是父类的名字
    NSString *superClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@",kJPBlockKVOClassPrefix,superClassName];
    Class newClass = NSClassFromString(newClassName);
    //类是否存在
    if (!newClass)  {//不存在需要创建类
        //1:申请类 父类、新类名称、额外空间
        newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
        //2:注册类
        objc_registerClassPair(newClass);
        //3:添加class方法,class返回父类信息 这里是`-class`
        SEL classSEL = NSSelectorFromString(@"class");
        Method classMethod = class_getInstanceMethod([self class], classSEL);
        const char *classTypes = method_getTypeEncoding(classMethod);
        class_addMethod(newClass, classSEL, (IMP)_jp_class, classTypes);
        
        //hook dealloc
        [[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
    }
    //4:添加setter方法
    SEL setterSEL = NSSelectorFromString(_setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)_jp_setter, setterTypes);
    
    return newClass;
}
复制代码

3.3 Hook注册和移除方法

对添加和移除的3个方法进行Hook处理:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self jp_methodSwizzleWithClass:self oriSEL:@selector(addObserver:forKeyPath:options:context:) swizzledSEL:@selector(jp_addObserver:forKeyPath:options:context:) isClassMethod:NO];
        [self jp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:context:) swizzledSEL:@selector(jp_removeObserver:forKeyPath:context:)isClassMethod:NO];
        [self jp_methodSwizzleWithClass:self oriSEL:@selector(removeObserver:forKeyPath:) swizzledSEL:@selector(jp_removeObserver:forKeyPath:)isClassMethod:NO];
    });
}
复制代码

因为removeObserver:forKeyPath:底层调用的不是removeObserver:forKeyPath:context:所以两个方法都要Hook。 那么我们又如何去怎么判断observer对应的keyPath是否存在。由于observationInfo存储的是私有类,那么可以直接通过kvc获取值:

- (BOOL)keyPathIsExist:(NSString *)sarchKeyPath observer:(id)observer {
    BOOL findKey = NO;
    id info = self.observationInfo;
    if (info) {
        NSArray *observances = [info valueForKeyPath:@"_observances"];
        for (id observance in observances) {
            id tempObserver = [observance valueForKey:@"_observer"];
            if (tempObserver == observer) {
                NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
                if ([keyPath isEqualToString:sarchKeyPath]) {
                    findKey = YES;
                    break;
                }
            }
        }
    }
    return findKey;
}
复制代码

Hook方法的具体实现如下:

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
        return;
    }
    [self jp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context {
    if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
        [self jp_removeObserver:observer forKeyPath:keyPath context:context];
    }
}

- (void)jp_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    if ([self keyPathIsExist:keyPath observer:observer]) {//key存在才移除
        [self jp_removeObserver:observer forKeyPath:keyPath];
    }
}
复制代码

这样就解决了重复添加和移除的问题。

3.4 自动移除观察者

重复添加和移除的问题已经解决了,那么还有一个问题是dealloc的时候自动移除。这块思路与自定义kvo相同,可以通过Hook观察者的的dealloc实现。

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
        return;
    }
    NSString *className = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
    Class newClass = NSClassFromString(newClassName);
    if (!newClass) {//类不存在的时候进行 hook 观察者 dealloc
        //hook dealloc
        [[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
    }
    [self jp_addObserver:observer forKeyPath:keyPath options:options context:context];
}

- (void)jp_dealloc {
    [self jp_removeSelfAllObserverd];
    [self jp_dealloc];
}
复制代码

如果kvo子类已经存在的时候,那么说明已经hook过了。 在deallocself.observationInfo是获取不到信息的,observationInfo是存储在被观察者中的。所以还需要我们自己存储下信息。

static NSString *const kJPSafeKVOObserverdAssiociateKey = @"JPSafeKVOObserverdAssiociateKey";

@interface JPSafeKVOObservedInfo : NSObject

@property (nonatomic, weak) id observerd;
@property (nonatomic, copy) NSString  *keyPath;
@property (nonatomic, strong) id context;

@end

@implementation JPSafeKVOObservedInfo

- (instancetype)initWitObserverd:(NSObject *)observerd forKeyPath:(NSString *)keyPath context:(nullable void *)context {
    if (self=[super init]) {
        _observerd = observerd;
        _keyPath = keyPath;
        _context = (__bridge id)(context);
    }
    return self;
}

@end

- (void)jp_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    if ([self keyPathIsExist:keyPath observer:observer]) {//observer 观察者已经添加了对应key的观察,再次添加不做处理。
        return;
    }
    NSString *className = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",className];
    Class newClass = NSClassFromString(newClassName);
    if (!newClass) {//类不存在的时候进行 hook 观察者 dealloc
        //hook dealloc
        [[observer class] jp_methodSwizzleWithClass:[observer class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(jp_dealloc) isClassMethod:NO];
    }
    
    //保存被观察者信息
    JPSafeKVOObservedInfo *kvoObservedInfo = [[JPSafeKVOObservedInfo alloc] initWitObserverd:self forKeyPath:keyPath context:context];
    NSMutableArray *observerdArray = objc_getAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPSafeKVOObserverdAssiociateKey));
    if (!observerdArray) {
        observerdArray = [NSMutableArray arrayWithCapacity:1];
    }
    [observerdArray addObject:kvoObservedInfo];
    objc_setAssociatedObject(observer, (__bridge const void * _Nonnull)(kJPSafeKVOObserverdAssiociateKey), observerdArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    //调用原始方法
    [self Jp_addObserver:observer forKeyPath:keyPath options:options context:context];
}
复制代码

那么我们现在就可以在jp_dealloc中主动的去调用移除

- (void)jp_dealloc {
    [self jp_removeSelfAllObserverd];
    [self jp_dealloc];
}

- (void)jp_removeSelfAllObserverd {
    NSMutableArray *observerdArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kjPSafeKVOObserverdAssiociateKey));
    for (jPSafeKVOObservedInfo *info in observerdArray) {
        if (info.observerd) {
            //调用系统方法,已经hook了,走hook逻辑。
            if (info.context) {
                [info.observerd removeObserver:self forKeyPath:info.keyPath context:(__bridge void * _Nullable)(info.context)];
            } else {
                [info.observerd removeObserver:self forKeyPath:info.keyPath];
            }
        }
    }
}
复制代码

这样的话就会在dealloc的时候,就会自己主动清空,已经释放掉的observer观察者了。

4. 总结

  • 观察多个属性的时候,以及新值和旧值,都要观察以及传递了context的情况下就无效,需要保存观察者相关的信息,就可以创建一个新类JPKVOInfo保存
  • 创建了观察者就需要去移除观察者
  • 可以通过Hook观察者的的dealloc方法,实现自动移除。

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

文章分类
iOS
文章标签