美文网首页iOS
iOS底层探索之KVO(四)—自定义KVO

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

作者: 俊而不逊 | 来源:发表于2021-08-08 09:15 被阅读0次

    回顾

    上篇博客已经自定义了KVO,但是还没有完善,还有些问题需要解决,这么本篇博客就把自定义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_addObserverjp_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底层探索之KVO(四)—自定义KVO

        本文链接:https://www.haomeiwen.com/subject/uygnvltx.html