本篇提纲
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;
结果:
每点击一次屏幕,会调用两次,因为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的验证
通过下断点发现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有没有联系
我们先来通过打印他的父类和子类来看看他和主类有没有关联。
通过打印可以了解到
NSKVONotifying_LGPerson
是LGPerson
的子类。
NSKVONotifying_LGPerson中都有什么内容
我们通过以下代码进行类NSKVONotifying_LGPerson的方法输出:
unsigned int intCount;
Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LGPerson"), &intCount);
for (unsigned int intIndex=0; intIndex
可以看到 NSKVONotifying_LGPerson
类重写了方法setNick:、class、dealloc、_isKVOA
我也分别打印了类NSKVONotifying_LGPerson
的协议、属性还有成员变量,都是空的,所以这个类中主要含有方法。
NSKVONotifying_LGPerson什么时候销毁?
通过前面的探索我们了解到NSKVONotifying_LGPerson
是在addObserve的时候动态创建的,那么会不会在关于KVO的api,在remove的时候销毁呢?我们来验证一下。
由此可见,self.person的isa指向在调用完remove之后指回了原来的类,但是此时获取NSKVONotifying_LGPerson还在,没有被销毁。
总结
- 通过addObserve动态创建了一个子类
NSKVONotifying_XXX
。 - 这个子类重写了属性的set方法,还有系统的delloc,class,_isKVOA方法。
- setter会调用父类原来的方法进行赋值,完成后进行回调通知。
- 移除kvo时,属性的isa指向了原来的类,并且
NSKVONotifying_XXX
还存在没有被销毁。
遗留问题:KVO动态创建的子类不是在delloc中被销毁的,那么是在什么时候销毁的?