美文网首页
20.iOS底层学习之KVO 原理

20.iOS底层学习之KVO 原理

作者: 牛牛大王奥利给 | 来源:发表于2021-11-15 12:43 被阅读0次

    本篇提纲
    1、KVO简介;
    2、KVO的使用;
    3、KVO的一些细节;
    4、KVO的底层原理;

    KVO简介

    KVO全称Key-Value Observing(键值观察),是允许对象在其他对象的属性发生更改是接到通知的一种途径。
    想要去了解KVO,要先理解KVC。KVO是在KVC的基础上实现的。

    KVO的使用

    KVO的使用分为以下三步:

    • Registering as an Observer(注册一个观察者)
      一个观察者对象可以通过发送addObserver:forKeyPath:options:context:方法,把自己作为观察者传进去,key path是要被观察的属性,options和context参数是用来管理通知的。

    Options:(指定为选项常量的按位或)会影响通知中提供的更改字典的内容以及生成通知的方式。
    通过指定选项NSKeyValueObservingOptionOld,可以选择从更改之前接收观察到的属性的值。使用选项NSKeyValueObservingOptionNew请求属性的新值。通过这些选项中的按位或,可以同时接收旧值和新值。

    Context:这个参数将在发生变化的通知调用时回传,可以是任意数据。也可以传NULL,这个时候接受到通知的时候只能通过key path这个参数来判断监听的是哪个监听生效了,但是也有可能两个类观察同一个属性,可能会导致区分不清楚。

    使用示例:

    static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
     [account addObserver:self forKeyPath:@"balance" options:(NSKeyValueObservingOptionNew |
    NSKeyValueObservingOptionOld) context:PersonAccountBalanceContext];
    

    这样接受到消息的时候,就可以通过context的值来判断是哪个观察者监听的生效了。

    说明:键值观察方法observer:forKeyPath:options:context:method不会强持有观察的对象,被观察者,或者context,所以如果有需要你要强持有观察的对象,被观察者,或者context,避免被回收。

    • Receiving Notification of a Change(接收到发生变化的通知)
      当观察的属性发生了改变的时候,观察者会收到消息observeValueForKeyPath:ofObject:change:context:,所有的观察者,必须实现这个方法。(不实现会crash)

    使用示例:

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 
        if (context == PersonAccountBalanceContext) {
            // Do something with the balance…
     
        } else if (context == PersonAccountInterestRateContext) {
            // Do something with the interest rate…
     
        } else {
            // Any unrecognized context must belong to super
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                   context:context];
        }
    }
    
    • Removing an Object as an Observer (移除作为观察者的对象)
      通过方法removeObserver:forKeyPath:context:可以移除键值观察。
      使用示例:
    [account removeObserver:self
                     forKeyPath:@"balance"
                        context:PersonAccountBalanceContext];
     
     [account removeObserver:self
                     forKeyPath:@"interestRate"
                        context:PersonAccountInterestRateContext];
    

    KVO的一些细节

    • KVO的手动实现
      在被观察者中实现automaticallyNotifiesObserversForKey方法,可以控制KVO是否自动通知。
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        NSLog(@"%s key:%@",__func__,key);
        return NO;
    }
    

    返回NO的时候自动通知关闭,返回YES的时候开启。

    再通过在set方法中实现willChangeValueForKey & didChangeValueForKey两个方法,完成手动通知。

    - (void)setName:(NSString *)name {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
    

    注意手动通知不受自动开关状态的影响。如果开关打开,并且也手动实现,那么接受方法会触发两次。

    • keyPathsForValuesAffectingValueForKey

    其值影响键控属性值的属性返回一组键路径。当key path的观察者向接收类的实例注册时,KVO本身会自动观察同一实例的所有key path路径,并在任何key path路径的值更改时向观察者发送key path更改通知。
    这个方法会返回一个NSSet,里面的元素是可能影响到监听的属性的属性。也就是说NSSet返回的属性发生改变的时候,也会触发KVO的通知消息。

    使用示例:
    本类实现。

    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"downloadProgress"]) {
            NSArray *affectingKeys = @[@"totalData", @"writtenData"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    
    - (NSString *)downloadProgress{
        if (self.writtenData == 0) {
            self.writtenData = 10;
        }
        if (self.totalData == 0) {
            self.totalData = 100;
        }
        return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
    }
    

    调用

    //添加监听
        [self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    //触发
        self.person.writtenData += 10;
        self.person.totalData  += 1;
    

    结果:


    image.png

    每点击一次屏幕,会调用两次,因为writtenData的值改变触发一次,totalData值改变再触发一次。

    • KVO对可变数组的观察
      按照属性的方法进行观察,示例:
    //注册
    self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
        [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    //修改值
           [self.person.dateArray addObject:@"1"];
    

    以上实现最终没有办法触发KVO,这是因为KVO是基于KVC的基础上进行实现的,数组的addObject的底层实现如下:

    - (id)addObject:anObject{
        return [self insertObject:anObject at:numElements];
    }
    
    - (id)insertObject:anObject at:(unsigned)index
    {
        register id *this, *last, *prev;
        if (! anObject) return nil;
        if (index > numElements)
            return nil;
        if ((numElements + 1) > maxElements) {
        volatile id *tempDataPtr;
        /* we double the capacity, also a good size for malloc */
        maxElements += maxElements + 1;
        tempDataPtr = (id *) realloc (dataPtr, DATASIZE(maxElements));
        dataPtr = (id*)tempDataPtr;
        }
        this = dataPtr + numElements;
        prev = this - 1;
        last = dataPtr + index;
        while (this > last) 
        *this-- = *prev--;
        *last = anObject;
        numElements++;
        return self;
    }
    

    所以可变数组不是对元素操作的,而是对index和length的操作,当(numElements + 1) > maxElements会重新开辟新的空间。没有KVC相关方法流程的查找。所以调用addObject不会触发。

    系统提供了调用方法:

        [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
    

    通过以上方法添加数组元素,KVO触发。

    KVO的底层原理

    根据官方文档的介绍可以了解到:

    Automatic key-value observing is implemented using a technique called isa-swizzling.

    自动键值观察的实现使用的技术叫做isa-swizzling

    isa-swizzling

    isa指针,是用来指向对象所属的包含一个派发表的类。这个表大体上有指向类的实现方法,还有其他的数据。
    当一个观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向一个中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。
    决不能依赖isa指针来确定类成员身份。相反,应该使用class方法来确定对象实例的类。

    • KVO使用isa-swizzling的验证
      image.png
      通过下断点发现isa的值已经不是LGPerson了,它变了🐳,变成了一个叫NSKVONotifying_LGPerson
      确实进行了isa指向的交换。

    下面我们来进一步深入了解下具体的过程,分为以下几个部分:

    • NSKVONotifying_LGPerson是什么时候创建的?
    • NSKVONotifying_LGPerson和原来的类LGPerson有没有联系?
    • NSKVONotifying_LGPerson中都有什么内容?
    • NSKVONotifying_LGPerson什么时候销毁?
    NSKVONotifying_LGPerson是什么时候创建的?

    我们分别在addObserver的前后打印person对象的isa指向的类,以及通过API去获取NSKVONotifying_LGPerson类:

    NSLog(@"addObserver之前:%s", object_getClassName(self.person));
        NSLog(@"addObserver之前:%s, %@", object_getClassName(self.person),objc_getClass("NSKVONotifying_LGPerson"));
        [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
        NSLog(@"addObserver之后:%s, %@", object_getClassName(self.person), objc_getClass("NSKVONotifying_LGPerson"));
    

    打印结果如下:


    由此可见在addObserver之后NSKVONotifying_LGPerson才被创建的,在这之前,这个类是为空的。
    NSKVONotifying_LGPerson和原来的类LGPerson有没有联系

    我们先来通过打印他的父类和子类来看看他和主类有没有关联。

    image.png
    通过打印可以了解到NSKVONotifying_LGPersonLGPerson的子类。
    NSKVONotifying_LGPerson中都有什么内容

    我们通过以下代码进行类NSKVONotifying_LGPerson的方法输出:

     unsigned int intCount;
        Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LGPerson"), &intCount);
        for (unsigned int intIndex=0; intIndex<intCount; intIndex++) {
            Method method = methodList[intIndex];
            NSLog(@"SEL:%@,IMP:%p",NSStringFromSelector(method_getName(method)), method_getImplementation(method));
        }
    
    image.png

    可以看到 NSKVONotifying_LGPerson类重写了方法setNick:、class、dealloc、_isKVOA
    我也分别打印了类NSKVONotifying_LGPerson的协议、属性还有成员变量,都是空的,所以这个类中主要含有方法。

    NSKVONotifying_LGPerson什么时候销毁?

    通过前面的探索我们了解到NSKVONotifying_LGPerson是在addObserve的时候动态创建的,那么会不会在关于KVO的api,在remove的时候销毁呢?我们来验证一下。

    image.png
    由此可见,self.person的isa指向在调用完remove之后指回了原来的类,但是此时获取NSKVONotifying_LGPerson还在,没有被销毁。

    总结

    • 通过addObserve动态创建了一个子类NSKVONotifying_XXX
    • 这个子类重写了属性的set方法,还有系统的delloc,class,_isKVOA方法。
    • setter会调用父类原来的方法进行赋值,完成后进行回调通知。
    • 移除kvo时,属性的isa指向了原来的类,并且NSKVONotifying_XXX还存在没有被销毁。

    遗留问题:KVO动态创建的子类不是在delloc中被销毁的,那么是在什么时候销毁的?

    相关文章

      网友评论

          本文标题:20.iOS底层学习之KVO 原理

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