一、KVO 简介
KVO(Key-Value Observing)是iOS提供的一种监听属性变化的机制。
二、使用场景
基本使用:
- 添加观察者
任意定义一个包含了属性的类:
@interface KVO : NSObject
@property (nonatomic, assign) NSUInteger count;
@property (nonatomic, copy) NSString *name;
@end
添加一个对上述类实例对象的属性值监听者:
KVO *kvoObj = [KVO new];
[kvoObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
上述代码对kvoObj的name属性进行了监听,其中监听的策略是NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld,表示属性值改变时通知内容里包含新的值和老的值;
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01, //指明接受通知方法参数中的change字典中应该包含改变后的新值。
NSKeyValueObservingOptionOld = 0x02, //指明接受通知方法参数中的change字典中应该包含改变前的旧值。
NSKeyValueObservingOptionInitial = 0x04, //当指定了这个选项时,在addObserver:forKeyPath:options:context:消息被发出去后,甚至不用等待这个消息返回,监听者对象会马上收到一个通知
NSKeyValueObservingOptionPrior = 0x08 //当指定了这个选项时,在被监听的属性被改变前,监听者对象就会收到一个通知
}
另外,context这里直接用nil,其实这个参数可以用来传值或者使用静态变量来标志一个指定的通知。
- 添加观察者通知响应函数
需要重写非正式协议NSKeyValueObserving的下述方法以接收属性值改变时发出的通知:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
这里只简单的打印出change里的信息;
- 属性改变时通知观察者
kvoObj.name = @"newName";
运行结果:
打印出了change字典对象里的内容,new和old分别对应NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld选项;
其中,kind = 1表示改变的类型为设置类型,具体代码对应的类型如下:
enum {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2, //针对集合类型属性,表示插入元素到该集合对象
NSKeyValueChangeRemoval = 3, //同上,表示移除元素从该集合对象
NSKeyValueChangeReplacement = 4 //同上,表示从该集合对象属相总替换元素
};
typedef NSUInteger NSKeyValueChange;
需要注意,添加监听的方法addObserver:forKeyPath:options:context:并不会对监听和被监听的对象以及context做强引用,你必须自己保证他们在监听过程中不被释放。
- 移除观察者
当不再需要监听该属性的时候,或者观察者需要被释放前,需要从被观察者队列中移除,否则被观察者继续发送通知则会导致野指针程序崩溃,具体实现如下:
- (void)dealloc {
[kvoObj removeObserver:self forKeyPath:@"name"];
}
三、自己实现KVO
苹果官方文档:
Automatic key-value observing is implemented using a technique called
isa-swizzling...When an observer is registered for an attribute of an
object the isa pointer of the observed object is modified, pointing to
an intermediate class rather than at the true class. As a result the
value of the isa pointer does not necessarily reflect the actual class
of the instance.
大概意思就是说利用了isa_swizzling技术,大家都知道swizzling是一种OC级别的Hook技术,所以isa_swizzling就是一种isa Hook技术,在一个支持KVO的对象被添加了观察者,系统会为其生成一个子类,重写了setXXX方法(XXX为被监听的属性名),并将该实例的Isa指针指向了新的这个子类(class),这样对被观察者进行属性赋值的时候调用的是重写后的setXXX方法,而setXXX方法内部添加了通知机制;
那么我们自己手动来实现一个简单的KVO:
- 3.1 自己实现一个添加观察者的方法:
//
// SUKVO.h
// DreamOneByOne
//
// Created by He on 2017/7/16.
// Copyright © 2017年 Sevenuncle. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol SUKVODelegate <NSObject>
-(void)su_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
@end
@interface SUKVO : NSObject
@property (copy, nonatomic) NSString *desc;
- (void)su_addObserver:(NSObject *)observer forKeyPath:(NSString *)path options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
模仿NSKeyValueObserving协议(分类)定义添加观察者的方法以及一个代理用于接收通知。下面是其内部实现:
// SUKVO.m
#import "SUKVO.h"
#import "SUKVO_SubClass.h"
#import <objc/message.h>
@implementation SUKVO
- (void)su_addObserver:(NSObject *)observer forKeyPath:(NSString *)path options:(NSKeyValueObservingOptions)options context:(void *)context {
//动态添加观察者对象
objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//动态改变被观察者isa指针,使访问到改变后的setXXX方法
object_setClass(self, [SUKVO_SubClass class]);
}
@end
当添加入一个观察者时,利用了runtime动态添加属性接口将该观察者加入该被观察实例对象上(实际上需要维护一个队列,用于记录所有观察该属性的观察者,这里为了简单),用于后续监听属性改变时给这个观察者发送消息;
然后,就是同样利用runtime改变该被观察者实例对象的isa指针(class),这样后续发送消息给被观察者均是往SUKVO_SubClass定义的方法里找(系统实现的子类为NSKVONotifying_原有类);
- 3.2 接下来看一下SUKVO_SubClass子类的实现:
#import "SUKVO_SubClass.h"
#import <objc/message.h>
@implementation SUKVO_SubClass
- (void)setDesc:(NSString *)desc {
[self willChangeValueForKey:@"desc"]; //改变之前通知
[super setDesc:desc];
[self didChangeValueForKey:@"desc"]; //改变之后通知
//通知值改变,这里为了图方便,简单的直接发送改变通知,实际上系统的实现利用了消息通知机制
id observer = objc_getAssociatedObject(self, "observer");
if([observer respondsToSelector:@selector(su_observeValueForKeyPath:ofObject:change:context:)]) {
[observer su_observeValueForKeyPath:@"desc" ofObject:self change:nil context:nil];
}
}
@end
这样就实现了简单的KVO,不过系统为了满足可以添加多个观察者监听同一个属性的需求,不能像上述实现的这么简单,需要一个一个字典加队列,这样每一个属性对应一个观察者队列,然后由内部一个通知中心统一给观察者发送通知;
四、与 KVC 的关系
关于KVO和KVC之间是否有联系,在网络上搜索了一通,也没个定论,不过大众普遍认为KVO和KVC通常是有联系的;但是,当了解了KVO的实现机制后,如上面自己实现KVO中,发现并未用到KVC,所以部分人开始怀疑KVO真的是基于KVC实现的吗?
那么试想一个问题:
对于一个包含只读(readonly)属性的变量,为什么也能通过setVaule:forKey进行赋值?因为对于默认readonly属性,系统是不会生成set的属性赋值方法的?那根据KVO的原理,是无法进行键值改变的监听的?
为了验证上面这个问题,我们需要确认两个事情:
- 对于一个readonly属性并且同时对该属性进行了观察者监听,是否有setXXX方法?
- 是否能够使用KVC对readonly属性赋值?
如果上述不包含setXXX方法并且能够使用KVC对只读属性赋值,就说明KVC内部包含了对KVO的支持!
- 下面开始验证第一个问题:
下面的KVCObject类包含了一个只读属性readonly和一个读写的属性,
@interface KVCObject : NSObject
@property (nonatomic, assign) NSUInteger count;
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, copy) NSString *location;
@end
同时对一个KVCObject实例对象添加了对name属性进行监听的观察者:
kvcObj = [KVCObject new];
[kvcObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[kvcObj addObserver:self forKeyPath:@"location" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
打印出此时kvcObj所在的类包含的实例方法:
KVCObject *tmpObj = kvcObj;
Class currentClass = object_getClass(tmpObj);
unsigned int methodCount;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
int i = 0;
for (; i < methodCount; i++) {
NSLog(@"%@ - %@", [NSString stringWithCString:class_getName(currentClass) encoding:NSUTF8StringEncoding], [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding]);
}
if( [tmpObj respondsToSelector:@selector(setName:)]) {
NSLog(@"name");
}
if( [tmpObj respondsToSelector:@selector(setLocation:)]) {
NSLog(@"location");
}
上面的代码打印出了一个对象添加了监听之后,生成的新的子类包含的实例方法列表:
可以看出,读写属性NSString * location生成了对应的setXXX方法,而只读属性name没有生成对应的setXXX方法,所以第一个问题得到验证。
- 验证第二个问题:此时通过KVC改变只读属性的值,能够得到KVO值改变通知?
[kvcObj setValue:@"newName" forKey:@"name"];
监听回调方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@", change);
}
输出结果:
可以看出,只读属性name在没有setName方法的情况下,通过KVC改变值得方式也得到了KVO通知,所以可以下结论,KVC内部的实现机制支持了KVO,KVO是依赖KVC的,并不像部分人怀疑的KVC和KVO之间毫无联系。