20.iOS底层学习之KVO 原理

本篇提纲
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 *)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
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 原理)