天天看点

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

回顾

在上篇博客已经自定义了

KVO

,但是还没有完善,还有些问题需要解决,这么本篇博客就把自定义

KVO

进行完善。

iOS底层探索之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

    以及

    observer

    context

    确定要移除的关联对象数据。
  • 当关联对象中没有数据的时候

    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的优化

在上面的做法中,有不合理的问题存在:就是在

+ load

Hook 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

子类的时候进行

hoo

k。显然在创建子类的时候更合适。代码如下所示:

//申请类-注册类-添加方法
- (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

过了。

dealloc

self.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

    方法,实现自动移除。

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

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

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