20.iOS底层学习之KVO 原理

2021-11-15  本文已影响0人  牛牛大王奥利给

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

KVO简介

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

KVO的使用

KVO的使用分为以下三步:

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,避免被回收。

使用示例:

- (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];
    }
}
[account removeObserver:self
                 forKeyPath:@"balance"
                    context:PersonAccountBalanceContext];
 
 [account removeObserver:self
                 forKeyPath:@"interestRate"
                    context:PersonAccountInterestRateContext];

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"];
}

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

其值影响键控属性值的属性返回一组键路径。当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值改变再触发一次。

//注册
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方法来确定对象实例的类。

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

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还在,没有被销毁。

总结


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

上一篇下一篇

猜你喜欢

热点阅读