KVO通知对大多数iOS开发者来说,都不陌生;而且也是用的比较多的。
他的全称为Key-Value Observing
,
按照官方的解释定义:键值观察提供了一种机制,该机制允许将其他对象的特定属性的更改通知给对象。
KVO的应用
实现KVO三部曲:
1、注册通知
addObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>
2、实现通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{}
3、移除通知
removeObserver:<#(nonnull NSObject *)#> forKeyPath:<#(nonnull NSString *)#>
在注册通知时,很多人可能都不知道context
的作用,它是一个上下文,按照官方的解释来说:是防止不同原因而观擦到相同的键路径导致的问题,是一种更安全,更可扩展的方法是使用上下文确保您收到的通知是发给观察者的,而不是超类的。
而当通知结束之后,我们是要将通知进行移除的,否则会导致出现NSRangeException
异常;因为通知没有移除,当又一通知发送过来,将导致两个通知消息,而找不到相应的通知,会导致野指针崩溃。
手动和自动打开KVO
在有的情况下,我们不需要通知处理,而有的时候又需要通知处理,这时候就需要进行手动实现KVO了,看下面的代码:
监听属性值变化:
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [Person new];
//注册通知
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
}
//实现通知
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
//移除通知
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
}
而在Person类中,通过automaticallyNotifiesObserversForKey
关闭自动通知,使用willChangeValueForKey
和didChangeValueForKey
来打开和关闭通知:
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
路径处理通知
以下载为例,下载进度=已下载/总下载,当我们要监听下载进度时,需要同时关注已下载进度和总下载,那么我们可以将两个化为一个来实现监听,看代码:
首先在viewController中实现通知注册(实现和移除省略):
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
在Person类中实现两个方法:
+ (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];
}
当writtenData
和totalData
发生改变时,就会实现通知的效果;
数组观擦
对数组内容改变进行观察,在文档中有详细介绍,KVO建立在KVC的基础上:
- (void)viewDidLoad {
[super viewDidLoad];
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options: (NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"dateArray"];
}
在实现观察的NSKeyValueChangeKey
属性中,有几个类型:
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
从字面意思上就可以理解,第一个是值的设置,第二个是值的插入,第三个是删除,第四个是替换。
KVO的原理探索
首先我们来看一下KVO对成员变量和属性的监听:
在Person类中添加一个成员变量name
和属性nickName
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[Person alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"nickName";
self.person->name = @"name";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nickName"];
}
通过执行代码,发现KVO只会对属性进行观察;
那为什么KVO只对属性进行观察呢,就需要对比属性和成员变量的区别,属性多了一个setter
方法,那么就可以验证得到,KVO会监听setter
方法。其次会形成一个中间类(通过文档得知)。
那么如果会形成一个中间类,那么Person
类的isa
指向会发生变化。
看下图所示,在对person
类注册通知完成后,获取self.person
的类名,就得到了NSKVONotifying_Person
,因此可以了解到,它有一个派生类(子类)NSKVONotifying_Person,从Person->NSKVONotifying_Person
。

下面通过一个遍历类以及子类的函数来看一下通知前后的变化:
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
打印结果:
2020-10-28 15:21:37.858478+0800 002---KVO原理探讨[5919:438564] classes = (
Person,
)
2020-10-28 15:21:45.950467+0800 002---KVO原理探讨[5919:438564] classes = (
Person,
"NSKVONotifying_Person",
)
那么就可以验证是存在一个派生类。
下面继续探索一下类的方法变化,同样的通过一个函数来查看方法变化:
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
在viewDidLoad
中调用[self printClassAllMethod:objc_getClass("NSKVONotifying_Person")];
方法:
打印结果:

可以看到它有4个方法,其中dealloc
和_isKVOA
可以理解,一个是释放,一个是判断是否为KVO类;
那么其中的setNickName
方法是继承的还是重写的呢?
下面创建一个person
的继承类student
,打印一下student
的方法,如果是继承,那么两个类打印的setNickName
是一摸一样的,如果不是继承,那么就是不一样的。
而student
类并没有打印,因此可以得到结论NSKVONotifying_Person
的setNickName
方法是重写的,应该有实现的方法。
那么既然了解到有中间类的存在,那么在中间的的执行过程中,person
类的isa
会指向isa
,而最后是否会重新指回person
类呢?
我们在析构函数处打印一下self.person
的类名:

经过验证,在移除通知后,isa
会重新指向Person
类。
而经过验证,当NSKVONotifying_Person
类创建之后,就会一直存在。
网友评论