这是一篇简单又丰富的简书,周末愉快!
一、KVO概要及简单使用
KVO 就是一种监听,那是如何做到监听的呢?首先创建一个简单的 Class,代码如下:
#import <Foundation/Foundation.h>
@interface KVOObject : NSObject
// 姓名
@property (nonatomic, copy) NSString* name;
@end
#import "KVOObject.h"
@implementation KVOObject
@end
很简单, 就一个 Class,然后定义了一个 name 属性而已。
一个简单的试验如下:
// 创建一个 KVO 对象
KVOObject* kvObj = [[KVOObject alloc] init];
kvObj.name = @"HG";
// 添加 KVO 监听
[kvObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
kvObj.name = @"CoderHG";
// 移除 KVO 监听, 在 iOS 10之前不移除的话直接 crash, 之后的就没事了
[kvObj removeObserver:self forKeyPath:@"name"];
具体的监听方法代码如下:
// KVO 的系统监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"%@, %@, %@", keyPath, object, change);
}
以上就是一个简单而完整的 KVO 的使用场景,但是具体是什么原理呢?
二、KVO 的原理初步认识
都知道一个对象一旦添加了 KVO 监听,在本质上是系统动态的改变了该对象的 isa 指针。如果对 isa 不了解的话,可以看这个 OC 小专题。
想要知道 isa 有什么样的变动,先实现如下一个方法:
// 打印具体的 cls 中的方法信息
- (NSString*)printMethodNamesOfClass:(Class)cls {
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 返回类名与方法列表
return [NSString stringWithFormat:@"类名: %@ \n方法列表: %@", NSStringFromClass(cls), methodNames];
}
然后将以上的试验修改一下,如下:
// 常规用法
- (void)convention {
// 创建一个 KVO 对象
KVOObject* kvObj = [[KVOObject alloc] init];
kvObj.name = @"HG";
Class cls = object_getClass(kvObj);
NSString* isaInfo = [self printMethodNamesOfClass:cls];
NSLog(@"\n\n添加 KVO 之前:\n%@", isaInfo);
// 添加 KVO 监听
[kvObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
kvObj.name = @"CoderHG";
cls = object_getClass(kvObj);
isaInfo = [self printMethodNamesOfClass:cls];
NSLog(@"\n\n添加 KVO 之后:\n%@", isaInfo);
// 移除 KVO 监听, 在 iOS 10之前不移除的话直接 crash, 之后的就没事了
[kvObj removeObserver:self forKeyPath:@"name"];
}
日志有如下的打印:
添加 KVO 之前:
类名: KVOObject
方法列表: .cxx_destruct, name, setName:
与
添加 KVO 之后:
类名: NSKVONotifying_KVOObject
方法列表: setName:, class, dealloc, _isKVOA
说明在添加 KVO 监听之后,isa 指针的值确实是变了,具体变化为:
- 1、将之前的 KVOObject, 更换成 NSKVONotifying_KVOObject
- 2、在 NSKVONotifying_KVOObject 中重写了 setName:、class 与 dealloc 方法,以及添加了一个 _isKVOA 方法。
三、KVO 与 KVC 的那一份藕断丝连
关于 KVC,强烈建议看一下这篇文章KVC 的原理概述,接下来将会在这篇文章的基础上做介绍。如果不看的话,可能你很难理解我所说的 非常规 KVC 调用是什么意思。虽然,我在这里仅仅是用到了那么一丁点的内容。
首先创建一个 Class, 代码如下:
#import <Foundation/Foundation.h>
@interface KVO8KVCObject : NSObject
@end
#import "KVO8KVCObject.h"
@interface KVO8KVCObject ()
{
// 非常规试验
NSString* isGoddess;
}
@end
@implementation KVO8KVCObject
@end
那接下来,我们想要表达一个什么问题呢?
KVC 能否触发 KVO 监听?
看了上面的 KVO8KVCObject 定义,我即将使用一个 KVC 的非常规调用来介绍,具体代码如下:
// KVO 与 KVC 那一段藕断丝连的区域
- (void)kvo8kvc {
// 创建一个 KVO8KVC 对象
KVO8KVCObject* kvO_CObj = [[KVO8KVCObject alloc] init];
// 通过 KVC 赋值
[kvO_CObj setValue:@"KJ" forKey:@"goddess"];
Class cls = object_getClass(kvO_CObj);
NSString* isaInfo = [self printMethodNamesOfClass:cls];
NSLog(@"\n\n添加 KVO 之前:\n%@", isaInfo);
// 添加 KVO 监听
[kvO_CObj addObserver:self forKeyPath:@"goddess" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
// 通过 KVC 赋值
[kvO_CObj setValue:@"JK" forKey:@"goddess"];
cls = object_getClass(kvO_CObj);
isaInfo = [self printMethodNamesOfClass:cls];
NSLog(@"\n\n添加 KVO 之后:\n%@", isaInfo);
// 移除 KVO 监听, 在 iOS 10之前不移除的话直接 crash, 之后的就没事了
[kvO_CObj removeObserver:self forKeyPath:@"goddess"];
}
看一下具体的 Log 打印,如下:
添加 KVO 之前:
类名: KVO8KVCObject
方法列表: .cxx_destruct
与
goddess, <KVO8KVCObject: 0x60c00001b080>, {
kind = 1;
new = JK;
old = KJ;
}
与
添加 KVO 之后:
类名: NSKVONotifying_KVO8KVCObject
方法列表: class, dealloc, _isKVOA
可以得出结论:
KVC 能触发 KVO 监听。
看到这里,也推翻了之前的一个结论:KVO 的正常触发的入口是 setter 方法,其实不是这样的,就如同上面的这个实验,在 NSKVONotifying_KVO8KVCObject 与 KVO8KVCObject 中根本就没有其对应的 setter 方法。
四、面试题
KVO如何对集合类进行监听?
这个面试题主要针对的是一个集合类中元素变动的监听,比如一个数组如何如何监听到 添加、插入与删除。按照常规的 KVO 方式,是监听不到的,但是系统已经为我们准备了专门的 API。
具体的介绍,看一参考 InterviewKVOController 中的具体实现:
/** KVO如何对集合类进行监听?
1. 需要借助一个 Class (KVObject), 监听这个 Class 实例对象中的集合属性
2. 实际监听的是 mutableArrayValueForKey: 返回的集合类. 如同替身.
*/
本篇介绍,到这里就要告一段落了。在写本简书的时候,我有一个试验 Demo # OC2Nature,可以作为一个参考。具体请看 KVO 目录。
同时别忘了看看本专题的其它文章 OC 小专题
在之前也写过关于 KVO 的简书,虽然那时候理解还不是太深入,但是里面依旧是有新东西的。感兴趣的话可以去看看:
- 1、简单的KVO实现方式: 间接的实现 KVO 的功能。
- 2、KVO与Category :A. @property 语法在不同场景的语义以及注意事项。 B. Category中实现 KVO 监听。