美文网首页iOS开发
ios kvo 的实现原理,自定义kvo

ios kvo 的实现原理,自定义kvo

作者: BlackStar暗星 | 来源:发表于2019-09-25 17:18 被阅读0次

    kvo的使用:

    //注册
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    //回调
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
    //移除
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    

    原理:首先要知道 kvo 是通过 runtime 实现的

    在使用kvo观察某对象的时候(例:对象 Person),系统会通过 runtime 动态创建这个对象的子类 NSKVONotifying_Person,然后重写了 setter 和 class 方法 (重写一词不是很准确,我们可以认为是拦截或替换),重写的class方法会返回父类对象 Person 类,让我们以为就是对象Person, 并没有NSKVONotifying_Person这个类的存在。(重写class 方法有待考究,后面会说为什么有待考究)
    我们猜测 在NSKVONotifying_Person重写setter方法的时候,会调用系统提供的两个方法willChangeValueForKey和didChangeValueForKey,大致如下

    - (void)setXxx:(NSString *)xxx {
        [self willChangeValueForKey:@"xxx"];
        [super setValue:nameStr forKey:@"xxx"];
        [self didChangeValueForKey:@"xxx"];
    }
    

    willChangeValueForKey 和 didChangeValueForKey
    这两个方法会调用 observeValueForKeyPath ,用来通知 观察对象的value 变化(只有这两个方法都调用了,才会 触发 observeValueForKeyPath 的回调)

    思路虽然是这么个思路,但是其实系统实现kvo 不可能只是拦截下setter方法这么简单,具体是怎么实现的我也没看过源码,不清楚,但是我们却可以根据这种思路去模拟一个kvo。

    下面我们来验证大致理论,并根据其原理自定义KVO,以便更好地理解KVO实现原理。

    首先验证是否产生新的子类,很简单,addObserver后断点查看控制台 image.png 然后在observeValueForKeyPath回调后我们看下堆栈信息 image.png

    可以发现 KVO 的实现,是有一套自己专属类的,NSSetObjectValueAndNotify 是 OC 对象类型的属性的封装,即监听的属性如果是 Object 类型,则使用NSSetObjectValueAndNotify,如果是 int,则使用NSSetIntValueAndNotify ,还有一些其他的不一一例举。然后通过NSKeyValueObservingPrivate 调用 changeValueForKeys,最终调用 observeValueForKeyPath 通知observer回调。

    observeValueForKeyPath 其实就是一个协议,最终会用我们添加的observer去调用observeValueForKeyPath



    接下来我们来模拟下KVO的实现过程,以更好的理解kvo的实现原理

    首先runtime动态创建类,我们自定义一个类,以block的方式进行kvo的回调,这里我用的 SEL 作为 key (其实没多大意义,和用字符串差不多,本来是要用 SEL 做监听字段验证的,写着写着写忘了)

     [BSNotifacation BSNotifacationAddObsever:self object:person1 keyForSel:@selector(name) callBack:^(id  _Nonnull oldValue, id  _Nonnull newValue) {
          NSLog(@"\nnoti person1:\noldValue = %@\nnewValue = %@",oldValue,newValue);
     }];
    

    然后实现方法

    +(void)BSNotifacationAddObsever:(id)obsever object:(id)object keyForSel:(SEL )keyForSel callBack:(void(^)(id oldValue,id newValue))change{
    
        // ==================================================
        // 新建 notiobject 类,用于处理回调,动态生成对象 等操作
        // 思路:调用一次监听 就生成一个 自定义的监听对象
        // 监听对象初始化后,已经重写了被监听对象object的class方法和setter方法
        // 所以不要轻易调用 class ,应使用 object_getClass 获取 class ,
        // 以免使用的时候混淆 class 的真正面目
        // ==================================================
        BSNotifacationObject *notiObj = [BSNotifacationObject notiObjectWithObsever:obsever object:object keyForSel:keyForSel callBack:change];
        
        
        
        // ==================================================
        // 将 notiObj(BSNotifacationObject) 添加到数组中,然后 关联给 objcCls
        // 目的:这样一个实例对象的所有监听就都和他的 isa 相关联,
        // 回调的时候,可以根据isa指向的class直接拿到数组,
        // 再根据 方法名 去进行批量的block回调
        // ==================================================
        Class objcCls = object_getClass(object);
        
        NSMutableArray *mutNotiObj = objc_getAssociatedObject(objcCls, "notiObj");
        if (!mutNotiObj) {
            mutNotiObj = [NSMutableArray array];
        }
        [mutNotiObj addObject:notiObj];
        
        objc_setAssociatedObject(objcCls, "notiObj", mutNotiObj, OBJC_ASSOCIATION_RETAIN);
        
    //    NSLog(@"关联对象:%@",objc_getAssociatedObject(objcCls, "notiObj"));
    }
    

    然后对 自定义监听类 BSNotifacationObject.m 实现

    
    +(instancetype)notiObjectWithObsever:(id)obsever object:(id)object keyForSel:(SEL)keyForSel callBack:(void (^)(id, id))change{
        
       return [[[self class]alloc]initNotiObjectWithObsever:obsever object:object keyForSel:keyForSel callBack:change];
    }
    
    
    -(instancetype)initNotiObjectWithObsever:(id)obsever object:(id)object keyForSel:(SEL)keyForSel callBack:(void (^)(id, id))change{
        
        self = [super init];
        if (self) {
            
            self.change = change;
            self.obsever = obsever;
            self.object = object;
            self.keyForSel = keyForSel;
            self.propertyName = NSStringFromSelector(keyForSel);
            
            [self dynamicCreatObject];
            [self exchangeDealloc:self.object];
        }
        return self;
    }
    
    
    #pragma mark - 获取 setter 和 getter 方法
    
    // 根据 keyForSel 获取 setter name
    -(SEL)selForGetter{
        
        NSString *name = NSStringFromSelector(self.keyForSel);
        NSString *firsWord = [name substringWithRange:NSMakeRange(0, 1)];
        firsWord = firsWord.uppercaseString;
        
        NSString *otherWord = [name substringWithRange:NSMakeRange(1, name.length - 1)];
        NSString *selectorName = [@"get" stringByAppendingFormat:@"%@%@",firsWord,otherWord];
       
        return NSSelectorFromString(selectorName);
    }
    
    
    // 根据 keyForSel 获取 getter name
    -(SEL)selForSetter{
        
        NSString *name = NSStringFromSelector(self.keyForSel);
        NSString *firsWord = [name substringWithRange:NSMakeRange(0, 1)];
        firsWord = firsWord.uppercaseString;
        
        NSString *otherWord = [name substringWithRange:NSMakeRange(1, name.length - 1)];
    
        NSString *selectorName = [@"set" stringByAppendingFormat:@"%@%@:",firsWord,otherWord];
       
        return NSSelectorFromString(selectorName);
    }
    
    
    #pragma mark - 动态创建 子类 ,并为其添加方法
    -(void)dynamicCreatObject{
        
        /// 动态生成继承 self.object 的子类 BSNoti_object
        /// 如果 noti 类 不存在,则创建 noti 类
        Class class = object_getClass(self.object);
        NSString *notiClassName = NSStringFromClass(class);
        if (![notiClassName hasPrefix:@"BSNoti_"]) {
            notiClassName = [NSString stringWithFormat:@"BSNoti_%@",NSStringFromClass(class)];
        }
        const char *charClassName = [notiClassName cStringUsingEncoding:NSUTF8StringEncoding];
        
        Class notiClass = objc_getClass(charClassName);
        
        if (!notiClass) {
            notiClass = objc_allocateClassPair(class, charClassName, 0);
    
            objc_registerClassPair(notiClass);
        }
        self.notiClass = notiClass;
        // 将 object 的isa 指向 notiClass,
        // 即 调用 object 的时候 其实object已经变成了BSNoti_object
        object_setClass(self.object, notiClass);
    
    
        /// 如果方法还没有,则 给 notiClass 增加 方法
        if (![self notiClass:notiClass hasSel:[self selForSetter]]) {
           
            // 将 keyForSel 的 IMP 改成 自定义 BSSetter 方法
            Class superClass = class_getSuperclass(notiClass);
            Method keyMethod = class_getInstanceMethod(superClass, [self selForSetter]);
            const char *keyMethodType = method_getTypeEncoding(keyMethod);
           
            char argType[128] = {};
            method_getArgumentType(keyMethod, 2, argType, 128);
            
            NSString *typeName = [NSString stringWithUTF8String:argType];
    //        NSLog(@"method arg type = %@",typeName);
            
            // **********************************************************
            // 如果是 id 类型的赋值,则IMP为setterNew,
            // 如果是int类型,则是setterInt,
            // 其他类型如float、double、bool也需要单独写(目前只写了int类型)
            // int 的不能使用 float ,double 去赋值,数值会出错,暂不知道问题,
            // 但是猜测 float和double,int和bool 可以使用同一个IMP
            // **********************************************************
            IMP targetImp = (IMP)setterNew;
            if ([typeName isEqualToString:@"i"]) {
                targetImp = (IMP)setterInt;
            }
            
            // **********************************************************
            // IMP 可以使用 C method 和 OC method 两种方法获取
            // Method instanceMethod = class_getInstanceMethod([self class], @selector(BSSetter:));
            // IMP ocImp = (IMP)method_getImplementation(instanceMethod);
            // **********************************************************
            BOOL addSuccess = class_addMethod(notiClass, [self selForSetter], targetImp , keyMethodType);
            
            if (!addSuccess) {
                NSLog(@"添加方法失败");
            }
        }
        
        // **********************************************
        // 重写 class 方法,使子类的 class 方法返回 父类
        // ***********************************************
        SEL classSel = NSSelectorFromString(@"class");
        if (![self notiClass:notiClass hasSel:classSel]) {
            
            Class superClass = [self.object superclass];
            Method keyMethod = class_getInstanceMethod(superClass, @selector(class));
            const char *keyMethodType = method_getTypeEncoding(keyMethod);
    
            IMP classImp = class_getMethodImplementation([self class], @selector(BSNotiClass));
            class_addMethod(notiClass, classSel, classImp, keyMethodType);
        }
    }
    
    #pragma mark 用于判断 class 是否含有 某方法
    -(BOOL)notiClass:(Class)class hasSel:(SEL)sel{
      
        unsigned int count;
        
        Method *methodList = class_copyMethodList(class, &count);
        
        NSMutableArray *methodMut = [NSMutableArray array];
        for (int i = 0; i<count; i++) {
            Method method = methodList[i];
            SEL methodName = method_getName(method);
            [methodMut addObject:NSStringFromSelector(methodName)];
        }
        
    //    NSLog(@"\nmethodList=\n%@",methodMut);
        
        NSString *oldSetName = NSStringFromSelector(sel);
        if ([methodMut containsObject:oldSetName]) {
            return YES;
        }
        return NO;
    }
    
    
    
    
    #pragma mark - 重写 class 方法 和 setter 方法
    
    #pragma mark 重写 class 方法,返回父类的class
    -(Class)BSNotiClass{
        Class baseCls = object_getClass(self);
        Class supperCls = class_getSuperclass(baseCls);
        return  supperCls;
    }
    
    //#pragma mark 重写setter方法  OC版本 setter 方法(对象赋值)
    //-(void)BSSetter:(id)value{
    //
    //    Class objcClass = object_getClass(self);
    //    NSMutableArray *oldMut = objc_getAssociatedObject(objcClass, "notiObj");
    //    NSMutableArray *tempArr = [oldMut copy];
    //
    //
    //    NSMutableArray *targetMut = [NSMutableArray array];
    //    id oldValue = nil;
    //    SEL selector = nil;
    //
    //    for (BSNotifacationObject *bsNotiObj in tempArr) {
    //
    //        if (bsNotiObj.obsever) {
    //            if (self==bsNotiObj.object) {
    //                [targetMut addObject:bsNotiObj];
    //                oldValue = [bsNotiObj.object valueForKey:bsNotiObj.propertyName];
    //                selector = [bsNotiObj selForSetter];
    //            }
    //        }else{
    //            [oldMut removeObject:bsNotiObj];
    //        }
    //    }
    //
    //
    //    if (selector) {
    //        // 通知后 赋值
    //        Class superClass = [self class];
    //
    //        Method superSet = class_getInstanceMethod(superClass, selector);
    //        IMP superImp = method_getImplementation(superSet);
    //        ((void(*)(id, SEL, id))superImp)(self, selector, value);
    //
    //    }else{
    //        NSLog(@"监听对象不存在");
    //    }
    //
    //
    //    for (BSNotifacationObject *bsNotiObj in tempArr) {
    //        bsNotiObj.change(oldValue, value);
    //    }
    //}
    
    #pragma mark id 类型的 setter 方法,C语言方法
    void setterNew(id self , SEL _cmd , id value){
        
        Class objcClass = object_getClass(self);
        NSMutableArray *oldMut = objc_getAssociatedObject(objcClass, "notiObj");
        NSMutableArray *tempArr = [oldMut copy];
        
        NSMutableArray *targetMut = [NSMutableArray array];
        id oldValue = nil;
        SEL selector = nil;
    
        for (BSNotifacationObject *bsNotiObj in tempArr) {
    
            if (bsNotiObj.obsever) {
                if (self==bsNotiObj.object && [bsNotiObj selForSetter]==_cmd) {
                    [targetMut addObject:bsNotiObj];
                    oldValue = [bsNotiObj.object valueForKey:bsNotiObj.propertyName];
                    selector = [bsNotiObj selForSetter];
                }
            }else{
                [oldMut removeObject:bsNotiObj];
            }
        }
        
        
        if (selector) {
            // 通知后 赋值
            Class superClass = [self class];
            
            Method superSet = class_getInstanceMethod(superClass, selector);
            IMP superImp = method_getImplementation(superSet);
            ((void(*)(id, SEL, id))superImp)(self, selector, value);
            
        }else{
            NSLog(@"监听对象不存在");
        }
        
        
        for (BSNotifacationObject *bsNotiObj in targetMut) {
            bsNotiObj.change(oldValue, value);
        }
    }
    
    #pragma mark  int 类型的 setter 方法 C语言方法
    void setterInt(id self , SEL _cmd , int value){
        
        Class objcClass = object_getClass(self);
    
        NSMutableArray *oldMut = objc_getAssociatedObject(objcClass, "notiObj");
        NSMutableArray *tempArr = [oldMut copy];
    
        
        NSMutableArray *targetMut = [NSMutableArray array];
        NSString * oldValue = 0;
        SEL selector = nil;
        
        for (BSNotifacationObject *bsNotiObj in tempArr) {
    
            if (bsNotiObj.obsever) {
                if (self==bsNotiObj.object && [bsNotiObj selForSetter]==_cmd) {
                    [targetMut addObject:bsNotiObj];
                    oldValue = [bsNotiObj.object valueForKey:bsNotiObj.propertyName];
                    selector = [bsNotiObj selForSetter];
                }
            }else{
                [oldMut removeObject:bsNotiObj];
            }
        }
        
        
        if (selector) {
            //  通知后 赋值
            Class superClass = [self class];
            
            Method superSet = class_getInstanceMethod(superClass, selector);
            IMP superImp = method_getImplementation(superSet);
            ((void(*)(id, SEL, int))superImp)(self, selector, value);
            
        }else{
            NSLog(@"监听对象不存在");
        }
        
        for (BSNotifacationObject *bsNotiObj in targetMut) {
            bsNotiObj.change(oldValue, [NSString stringWithFormat:@"%d",value]);
        }
    }
    
    
    
    #pragma mark - 检测 obsever 是否存在,如果不存在,则把不需要监听的对象移除
    
    -(void)exchangeDealloc:(id)object{
        
    //    SEL dealloc = NSSelectorFromString(@"dealloc");
    //    Method oldDealloc = class_getInstanceMethod([self.obsever class], dealloc);
    //    Method ObsDealloc = class_getInstanceMethod([self class], @selector(obseverDealloc));
    //    method_exchangeImplementations(oldDealloc, ObsDealloc);
    //
    //    objc_setAssociatedObject(self.obsever, "noti_object", object, OBJC_ASSOCIATION_RETAIN);
    //    objc_setAssociatedObject(self.obsever, "bsnoti_obj", self, OBJC_ASSOCIATION_RETAIN);
    }
    
    /// 方法交换,obsever 的 dealloc 和 obseverDealloc 进行交换
    /// 主动 调用 dealloc会闪退,没找到解决方案,暂时不做
    -(void)obseverDealloc{
        
    //    id notiObj = objc_getAssociatedObject(self, "bsnoti_obj");
    //    [notiObj obseverDealloc];// 会闪退,因为主动调用了dealloc方法
    //
    //    id object = objc_getAssociatedObject(self, "noti_object");
    //    Class objClass = object_getClass(object);
    //    NSMutableArray *mutArr = objc_getAssociatedObject(objClass, "notiObj");
    //    NSArray *copyArr = [mutArr copy];
    //
    //    for (BSNotifacationObject *notiObj in copyArr) {
    //        if ([notiObj.obsever isEqual: self] || !notiObj.obsever) {
    //            [mutArr removeObject:notiObj];
    //        }
    //    }
    }
    
    


    出现的问题
    1、首先是 dealloc 无法通过 方法交换 去进行 observe 的销毁监听。如果无法对 observe 进行 dealloc 监听,那么我们就不能在最合适的时间移除 已经不在监听范围内的 监听对象 了(在重写的 setter 方法里 也是可以移除的,但是移除的时间点并不是最正确的时间点)

     for (BSNotifacationObject *bsNotiObj in tempArr) {
    
            if (bsNotiObj.obsever) {
                if (self==bsNotiObj.object && [bsNotiObj selForSetter]==_cmd) {
                    [targetMut addObject:bsNotiObj];
                    oldValue = [bsNotiObj.object valueForKey:bsNotiObj.propertyName];
                    selector = [bsNotiObj selForSetter];
                }
            }else{
                [oldMut removeObject:bsNotiObj];
            }
        }
    

    2、调用完 setClass 方法后,即便重写 class 方法,也不会达到系统 kvo 那样,只改变了 isa 指针,其他的不变。举例:


    自定义kvo
    系统kvo

    原因猜测,可能是我的 setClass 方法用的不正确,或者还有别的什么地方需要完善。但是。。。我不知道!

    3、由于kvo是需要重写setter 方法的,所以使用 _name = @"hello" 的方式是无法触发kvo的,我们必须使用 kvc方式或 self.name = @"hello"方式赋值才能触发kvo的回调

    相关文章

      网友评论

        本文标题:ios kvo 的实现原理,自定义kvo

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