自定义KVO

457 阅读9分钟

前面介绍KVO是讲了系统KVO实现的大体逻辑,分为下面几步

1.创建子类,将该对象的isa指向新创建的子类

2.重写class,指向父类(即当前对象的class)

3.重写指定键值的set的imp方法,实现监听回调

4.释放时,该对象指向当前类(即释放类的父类),避免不必要的麻烦

下面模仿系统实现自动销毁的自定义KVO

大体逻辑核心代码

注册子类方法

//注册新的子类
Class newCls = objc_allocateClassPair(cls, [NSString stringWithFormat:@"LSKVONotifying_%@", NSStringFromClass(cls)].UTF8String, 0);
    objc_registerClassPair(newCls);

子类重写dealloc

//子类重写dealloc方法实现
SEL deallocSel = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod(cls, deallocSel);
class_addMethod(newCls, deallocSel, (IMP)ls_dealloc, method_getTypeEncoding(deallocMethod));

子类重写class

//子类重写class方法实现
SEL classSel = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod(cls, classSel);
class_addMethod(newCls, classSel, (IMP)ls_class, method_getTypeEncoding(classMethod));

子类重写setter

//子类重写setter方法实现
SEL setterSel = NSSelectorFromString(setter);
Method setterMethod = class_getInstanceMethod(cls, setterSel);
class_addMethod(newCls, setterSel, (IMP)ls_setter, method_getTypeEncoding(setterMethod));

设置对象的类为新的类

//将但前对象所属类指向其子类
 object_setClass(observed, newCls);

基础方法就这么多,下面实现一个能用的吧

自定义KVO

如果理解了前面FBKVOController的观察者之间的对应关系,相信很快理解自定义KVO之间的对应关系,即一个被观察者对应多个观察者

_LSKVOInfo

通过上面可以得出,每个键值和对应block对应一个载体,其为回调的一个基本数据结构,我们定义为LSKVOInfo

其数据结构如下所示:

{
@public
    __weak id _observer; //观察者
    NSString *_keyPath; //监听键值
    CallBack _block; //回调block
    SEL _sel; //回调sel
}

_LSClassInfo

这个类负责处理被观察者的核心逻辑实现

{
@public
    Class _cls; //创建的新类
    Class _superCls; //原始类
    NSMutableDictionary<NSString *, __LSClassKeyPathInfo *> *_keyPathMap;//每次添加一对setter和getter键值,均指向__LSClassKeyPathInfo对象
    NSHashTable<id> *_hashTable; //该类作为被观察者对象的的弱引用集合,用于判断是否已经设置class了
    dispatch_semaphore_t _semaphore;
}

1.创建类

类只需创建一个,因此当我们创建一个观察者时,如果没创建该观察类,则创建该观察类,并且标记,方便下一次判断和使用;如果发现已经创建类,则直接获取当前类的class,不在进行创建,

2.重写dealloc、重写class

其和创建类一样,只需要重写一次,因此重写过程只需要在创建类的时候重写即可

创建类、dealloc、class实现如下所示:

- (void)_observerClass:(Class)cls info:(_LSKVOInfo *)info {
    //注册新的子类
    Class newCls = objc_allocateClassPair(cls, [NSString stringWithFormat:@"LSKVONotifying_%@", NSStringFromClass(cls)].UTF8String, 0);
    objc_registerClassPair(newCls);
    
    //子类重写dealloc方法实现
    SEL deallocSel = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod(cls, deallocSel);
    class_addMethod(newCls, deallocSel, (IMP)ls_dealloc, method_getTypeEncoding(deallocMethod));
    
    //子类重写class方法实现
    SEL classSel = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod(cls, classSel);
    class_addMethod(newCls, classSel, (IMP)ls_class, method_getTypeEncoding(classMethod));
    
    _superCls = cls;
    _cls = newCls;
}

#pragma mark --重写class方法,注意这里的self代表着什么(调用者对象,该类的应用对象为observed,即self为observed)
static Class ls_class(id self, SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}

#pragma mark --重写dealloc方法
static void ls_dealloc(id self, SEL _cmd) {
    //class设置回去,后面获取父类也可以[self class],可以避免不必要的麻烦
    object_setClass(self, class_getSuperclass(object_getClass(self)));
}

3.重写setter方法

和创建类一样,当前键值的setter方法未重写时,重写并保存记录;如果重写,则不再重新实现setter方法,保存相关的后面在介绍

//重写setter并保存
- (void)addSetterAndSave:(_LSKVOInfo *)info {
    NSString *setter = [NSString stringWithFormat:@"set%@%@:",[[info->_keyPath substringToIndex:1] uppercaseString], [info->_keyPath substringFromIndex:1]];
    __LSClassKeyPathInfo *keyPathInfo = [__LSClassKeyPathInfo alloc];
    keyPathInfo->_setter = setter;
    keyPathInfo->_getter = info->_keyPath;
    
    //没有加入key的情况加入key
    SEL sel = NSSelectorFromString(setter);
    const char *encoding = method_getTypeEncoding(class_getInstanceMethod(_superCls, sel));
    IMP imp = getTypeEncodingImp(encoding, &(keyPathInfo->_type));
    if (!imp) return;
    
    class_addMethod(_cls, sel, imp, encoding);

    //保存对应的键值对,方便获取
    [_keyPathMap setObject:keyPathInfo forKey:info->_keyPath];
    [_keyPathMap setObject:keyPathInfo forKey:setter];
}

4.给当前对象设置新的类,并且坐上标记,如果已经该对象已经设置了类,则不再设置, 设置setter方法也同理

- (void)updateInfo:(_LSKVOInfo *)info observer:(id)observed {
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    
    //设置对象的类为新的类
    if (![_hashTable containsObject:observed]) {
        object_setClass(observed, _cls);
        [_hashTable addObject:observed]; //加入集合中
    }
    
    //如果没有实现该键值的setter方法,则实现
    __LSClassKeyPathInfo *keyPathInfo = [_keyPathMap objectForKey:info->_keyPath];
    if (!keyPathInfo) {
        //子类重写setter方法实现
        addSetterAndSave(self, info);
    }
    //设置classInfo基本信息
    
    //获取对象对应的mapTable,对象对应的mapTable不一定存在
    NSMapTable *mapTable = objc_getAssociatedObject(observed, &kKVOAssociatedMapTableKey); //获取当前对象对应的
    if (!mapTable) {
        //初始化mapTable
        mapTable = [[NSMapTable alloc] initWithKeyOptions:(NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality) valueOptions:(NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality) capacity:0];
        //设置回调
        NSMutableDictionary *infos = [NSMutableDictionary dictionary];
        [infos setObject:info forKey:info->_keyPath];
        //根据观察者加入mapTable
        [mapTable setObject:infos forKey:info->_observer];
        //加入关联
        objc_setAssociatedObject(observed, &kKVOAssociatedMapTableKey, mapTable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }else {
        NSMutableDictionary *infos = [mapTable objectForKey:info->_observer];
        if (!infos) {
            infos = [NSMutableDictionary dictionary];
            //根据观察者加入mapTable
            [mapTable setObject:infos forKey:info->_observer];
        }
        [infos setObject:info forKey:info->_keyPath];
    }
    
    //给对象添加该类的关联
    if (!objc_getAssociatedObject(observed, &kKVOAssociatedClassKey)) {
        objc_setAssociatedObject(observed, &kKVOAssociatedClassKey, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    dispatch_semaphore_signal(_semaphore);
}

集合以及关联介绍:

看了前面除了基本操作,会发现多了很多集合类,下面介绍一下其功能:

_hashTable: 保存该类作为被观察者的弱引用集合,用于判断是否已经设置class了,由于NSHashTable弱引用类型,因此当对象释放时,自动会从集合中移除

_keyPathMap:每次添加一对setter和getter键值,均指向__LSClassKeyPathInfo对象

__LSClassKeyPathInfo: 保存着 setter、getter、setter方法参数的encodingTypes(用于重写setter方法)

mapTable:其为被观察者对象对应的,以每一个观察者为键值key,以keyPath对应的回调集合为value(方便直接获取),以Associated的方式关联到被观察者对象中

_classInfo: 其作为该类的类方便的信息(保存着类似于元类的一些处理方式),其也以Associated的方式被关联到每一个被观察者对象中

setter方法重写

看了上面重写setter方法的的实现后,会疑问,为什么要获取方法的encodingType,其主要是为了判断setter方法传递参数的类型,毕竟自己调用seter方法的时候,可能会有基本数据类型,例如:int long,也可能为NSObject类型,也可能为结构体等,这些是不可能使用id接收到的,因此要通过此类型来实现对应的imp

void addSetterAndSave(_LSClassInfo *classInfo, _LSKVOInfo *info) {
    NSString *setter = [NSString stringWithFormat:@"set%@%@:",[[info->_keyPath substringToIndex:1] uppercaseString], [info->_keyPath substringFromIndex:1]];
    __LSClassKeyPathInfo *keyPathInfo = [__LSClassKeyPathInfo alloc];
    keyPathInfo->_setter = setter;
    keyPathInfo->_getter = info->_keyPath;
    
    //没有加入key的情况加入key
    SEL sel = NSSelectorFromString(setter);
    const char *encoding = method_getTypeEncoding(class_getInstanceMethod(classInfo->_superCls, sel));
    IMP imp = getTypeEncodingImp(encoding, &(keyPathInfo->_type));
    if (!imp) return;
    
    class_addMethod(classInfo->_cls, sel, imp, encoding);

    //保存对应的键值对,方便获取
    [classInfo->_keyPathMap setObject:keyPathInfo forKey:info->_keyPath];
    [classInfo->_keyPathMap setObject:keyPathInfo forKey:setter];
}

获取参数类型:

以对象为例,获取的setter的encodingTyes如下所示:

v24@0:8@16  v--void 24代表大小,以此类推

其他的setter方法一直到@和前面的都一样,均为返回值void、参数id self、参数SEL _cmd,最后一个才是我们传递的方法参数,因此可以通过该方法获取,其类型对照表如下所示:

通过上面就可以获取到类型了,然后重写setter方法即可

重写setter方法的过程,注意新旧值的包装,例如:数字转化成NSNumber, 结构体转化成NSValue等,block由于不能转化,通过KVC获取到对象了类型

回调

直接遍历map集合进行遍历即可

void _ls_responseSetter(id self, id value, id oldValue, NSString *keyPath) {
    //响应当前对象添加的对应键值的所有监听
    NSMapTable *mapTable = objc_getAssociatedObject(self, &kKVOAssociatedMapTableKey);
    for (id observer in mapTable) {
        if (observer) {
            NSDictionary *infos = [mapTable objectForKey:observer];
            if (infos) {
                _LSKVOInfo *info = [infos objectForKey:keyPath];
                if (info) {
                    if (info->_block)
                        info->_block(observer, value, oldValue);
                    else if (info->_sel)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                        //这个由于不存在循环引用问题,则就不返回observer了
                        [observer performSelector:info->_sel withObject:value withObject:oldValue];
#pragma clang diagnostic pop
                }
            }
        }
    }
}

_KVOControllerClassManager

看名字即可察觉到,其为所有类的管理对象,为一个单例,其保存这所有的被观察类,根据类是否已经创建,执行更新还是创建方法,并避免一些创建安全问题

{
@public
    NSMutableDictionary<NSString *, _LSClassInfo *> *_classInfoMap; //类的集合
    dispatch_semaphore_t _semaphore;
}

//添加新的观察, initialResponse设置回调时,是否默认回调一次
- (void)observer:(id)observed info:(_LSKVOInfo *)info initialResponse:(BOOL)initialResponse {
    NSString *clsName = NSStringFromClass([observed class]);
    
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    
    _LSClassInfo *classInfo = [_classInfoMap objectForKey:clsName];
    
    if (classInfo) {
        dispatch_semaphore_signal(_semaphore);
        //根据对象更新classInfo的回调信息
        [classInfo updateInfo:info observer:observed];
    }else {
        classInfo = [[_LSClassInfo alloc] init];
        [_classInfoMap setObject:classInfo forKey:clsName];
        //添加新类
        [classInfo _observerClass:object_getClass(observed) info:info];
        
        dispatch_semaphore_signal(_semaphore);
        //根据对象设置classInfo的回调信息
        [classInfo setInfo:info observer:observed];
    }
    if (initialResponse) {
        [classInfo responseWithInfo:info observer:observed];
    }
}

NSObject (LSKVOController)

为用户使用类,有如下使用方式,可参考代码注释或者remind.md测试文件

其可以监听单个、多个keyPath

也可以监听某一个键值的指定子键值和所有子键值

支持监听后立即回调一次

/*
 注意: 只有属性才支持监听,即有setter和getter方法,想获取旧值必须有getter方法
 不支持的监听类型,包括自定义struct结构体,union联合体, c类型数组[]
 id对象返回的还是对象
 NSNumber、Class、SEL、Pointer(指针类型)、char *返回的均为NSValue类型
 */


/// 添加block监听
/// @param observer 观察者
/// @param keyPath 被观察的属性,会因为没有setter和getter方法而报错
/// @param callback 回调block(id observer, id newValue, id oldValue) 观察者、新值、旧值
/// @param initialResponse 是否默认回调一次
- (void)ls_addObserver:(id)observer keyPath:(NSString * _Nonnull)keyPath callBack:(CallBack)callback initialResponse:(BOOL)initialResponse;
//默认不回调
- (void)ls_addObserver:(id)observer keyPath:(NSString * _Nonnull)keyPath callBack:(CallBack)callback;


/// 给多个属性添加block监听
/// @param observer 观察者
/// @param keyPaths 被观察的属性集合,会因为没有setter和getter方法而报错
/// @param callback 回调block(id observer, id newValue, id oldValue) 观察者、新值、旧值
/// @param initialResponse 是否默认回调一次
- (void)ls_addObserver:(id)observer keyPaths:(NSArray<NSString *> * _Nonnull)keyPaths callBack:(CallBack)callback initialResponse:(BOOL)initialResponse;
//默认不回调
- (void)ls_addObserver:(id)observer keyPaths:(NSArray<NSString *> * _Nonnull)keyPaths callBack:(CallBack)callback;



/// 给指定键值属性添加子属性监听block,当属性的某个子属性更改时,则回调当前属性(指定键值属性的子属性不支持监听时,改变不会回调)
/// @param observer 观察者
/// @param keyPath 被观察的属性集合,会因为没有setter和getter方法而报错(子属性也必须有setter和getter方法)
/// @param callback 回调block(id observer, id newValue, id oldValue) 观察者、新值、旧值
/// @param initialResponse 是否默认回调一次
- (void)ls_addSubObserver:(id)observer keyPath:(NSString * _Nonnull)keyPath callBack:(SubCallBack)callback initialResponse:(BOOL)initialResponse;
//默认不回调
- (void)ls_addSubObserver:(id)observer keyPath:(NSString * _Nonnull)keyPath callBack:(SubCallBack)callback;



/// 给属性添加子属性监听block,指定键值的子属性更改时,则回调当前属性
/// @param observer 观察者
/// @param keyPath 观察的属性键值
/// @param subkeyPaths 被观察的属性的响应子属性白名单集合,会因为没有setter和getter方法而报错(子属性也必须有setter和getter方法)
/// @param callback 回调block(id observer, id newValue, id oldValue) 观察者、新值、旧值
/// @param initialResponse 是否默认回调一次
- (void)ls_addSubObserver:(id)observer keyPath:(NSString * _Nonnull)keyPath subKeyPaths:(NSArray<NSString *> *)subkeyPaths callBack:(SubCallBack)callback initialResponse:(BOOL)initialResponse;
//默认不回调
- (void)ls_addSubObserver:(id)observer keyPath:(NSString * _Nonnull)keyPath subKeyPaths:(NSArray<NSString *> *)subkeyPaths callBack:(SubCallBack)callback;



/// 添加SEL监听
/// @param observer 观察者
/// @param keyPath 被观察的键值,会因为没有setter和getter方法而报错
/// @param sel 回调SEL 两个参数 observer、newValue 新值、旧值
/// @param initialResponse 是否默认回调一次
- (void)ls_addObserver:(id)observer keyPath:(NSString * _Nonnull)keyPath selector:(nonnull SEL)sel initialResponse:(BOOL)initialResponse;
//默认不回调
- (void)ls_addObserver:(id)observer keyPath:(NSString * _Nonnull)keyPath selector:(nonnull SEL)sel;


/// 添加SEL监听
/// @param observer 观察者
/// @param keyPaths 被观察的键值集合,会因为没有setter和getter方法而报错
/// @param sel 回调SEL 两个参数 observer、newValue 新值、旧值
/// @param initialResponse 是否默认回调一次
- (void)ls_addObserver:(id)observer keyPaths:(NSArray<NSString *> * _Nonnull)keyPaths selector:(nonnull SEL)sel initialResponse:(BOOL)initialResponse;
//默认不回调
- (void)ls_addObserver:(id)observer keyPaths:(NSArray<NSString *> * _Nonnull)keyPaths selector:(nonnull SEL)sel;

最后

如果发现问题欢迎提出讨论哈