OC底层探索(十六) KVO底层原理
KVO
全称KeyValueObserving
,是苹果提供的一套事件通知机制
。允许对象监听
另一个对象特定属性的改变,并在改变时接收到事件
。由于KVO
的实现机制,所以对属性才会发生作用,一般继承自NSObject
的对象都默认支持KVO
。
KVO
和NSNotificationCenter
都是iOS中观察者
模式的一种实现。区别在于,相对于被观察者
和观察者
之间的关系,KVO
是一对一
的,而不一对多的。KVO
对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO
可以监听单
个属性的变化,也可以监听集合
对象的变化。通过KVC
的mutableArrayValueForKey:
等方法获得代理对象
,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。集合对象包含NSArray
和NSSet
。
KVO的使用
1、基本使用
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, strong) LGStudent *st;
@end
static void *PersonNickContext = &PersonNickContext;
@interface LGViewController ()
@property (nonatomic, strong) LGPerson *person;
@property (nonatomic, strong) LGStudent *student;
@end
@interface LGViewController ()
@property (nonatomic, strong) LGPerson *person;
@property (nonatomic, strong) LGStudent *student;
@end
@implementation LGViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
// 1: context : 上下文
[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];
}
@end
1.1 监听属性name
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
- context 上下文
在官方文档中有是这样介绍的
The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing.
百度翻译:
addObserver:forKeyPath:options:context: message中的上下文指针包含将在相应的更改通知中传递回观察者的任意数据。您可以指定NULL并完全依赖于键路径字符串来确定更改通知的来源,但是这种方法可能会给其超类出于不同的原因也在观察相同键路径的对象造成问题。
一种更安全、更可扩展的方法是使用content来确保接收到的通知是发送给观察者的,而不是超类。
类中唯一命名的静态变量的地址可以作为一个良好的上下文(content)。在超类或子类中以类似方式选择的上下文将不太可能重叠。您可以为整个类选择一个上下文,并依赖通知消息中的关键路径字符串来确定更改的内容。或者,您可以为观察到的每个键路径创建不同的上下文,这样就完全不必进行字符串比较,从而提高通知解析的效率。
- 就是说context就相当于通知中的那个key,以更方便、更安全、更可扩展的方式。注意:如果在添加监听时设置了context,那么删除时,也需要设置同样的context。例如:
static void *PersonNickContext = &PersonNickContext;
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNickContext) {
}
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"nick" context:PersonNickContext];
}
1.2 改变name的值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
}
1.3 触发的方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
// 性能 + 代码可读性
NSLog(@"%@",change);
}
1.4 移除监听
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];
2、数组观察
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
// 5: 数组观察
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// KVC 集合 array
[[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"];
}
3、路径方式
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [LGPerson new];
// 4: 路径处理
// 下载的进度 = 已下载 / 总下载
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.writtenData += 10;
self.person.totalData += 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:@"nick" context:NULL];
[self.person removeObserver:self forKeyPath:@"downloadProgress"];
}
@implementation LGPerson
// 下载进度 -- writtenData/totalData
+ (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];
}
@end
4、手动和自动开关
观察者默认是自动打开的,我们也可以手动打开首页在观察类中实现
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
再打开需要观察的对象
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
KVO底层探索
1、KVO观察属性
探索:OC在类中有属性和成员变量,那KVO观察的是属性还是成员变量呢?
- 定义LGPerson类,自定义成员变量name与属性nickName,源码如下:
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
- 在最上面的代码中再添加nickName的监听,查看打印结果
- 通过打印结果可知,
KVO
只对属性
进行监听
,对成员变量不监听
。-
属性
与成员变量
的区别
在于属性存在setter
、getter
方法,而成员变量没有。
-
2、添加完KVO后生成了中间类
2.1 修改ISA指向
- 分别添加KVO之前和KVO之后打一个断点,在控制台输出以下person对象的ISA。
(由于电脑不在,不方便调试截图,图片来自荒唐的天梯的博客)
- 根据打印的内容可以看到,在添加KVO之后,person对象的ISA指向了NSKVONotifying_LGPerson。
2.2 NSKVONotifying_LGPerson是LGPerson的分类
- 自定义查看本类和子类的方法
#pragma mark - 遍历类以及子类
- (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);
}
- 分别在添加KVO之前和KVO之后调用该方法,查看以下打印结果。
- 根据打印我们发现 NSKVONotifying_LGPerson是LGPerson的分类。
2.3 还原ISA指向
思考:person对象的ISA指向发生了改变,那还会不会在还原ISA指向呢?
- 分别在删除前和删除后打一个断点,在控制台分别打印person的ISA
- 根据打印我们发现删除后person的ISA又重新指向了LGPerson
思考: 那么isa还原后,NSKVONotifying_LGPerson会不会删除呢?
- 在删除后调用自定义的方法,查看打印,中间类是没有被移除的。
总结:
- 添加KVO后,对象的ISA指向了以NSKVONotifying_NSKVONotifying_开头的中间类,且是原类的分类。
- 在删除KVO后将ISA还原。
- 删除KVO后,生成的中间类不会被删除,以便下次使用。
3、探索中间类
3.1 查看中间类中的方法
- 自定义方法,打印输出中间类的所有的方法
#pragma mark - 遍历方法-ivar-property
- (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);
}
- 调用打印
-
从打印结果可知,在NSKVONotifying_LGPerson类中添加了四个方法,分别为:setNickName、class、dealloc、_isKVOA这四个方法。
- _isKVOA :判断当前是否为KVO类
- dealloc: 释放
- setNickName :nickName属性的setter方法
- class:clasaa方法
3.2 setNickName 中做了什么
探索setNikeName是重写还是继承
- 定义一个类,打印查看。
- 定义一个LGStudent,并继承与LGPerson,不重写setNikeName,打印查看发现并没有setNikeName的打印。
- 在探索类的结果时,methodlist只存放自己的方法,如果是继承,那么就需要去父类的methodList中查找。
重写setNikeName方法中具体做了什么呢?
大胆猜测一下,首先,在调用NSKVONotifying_LGPerson重写setter方法的时候,改变的是其父类LGPerson的nickName的值,那么在重写的setter方法中一定有对父类nickName进行传值的操作。
- 设置观察self->_pserson->_nickName,在控制台手动输入一个断点。具体命令为:
watchpoint set variable self->_person->_nickName
-
执行发现进入了断点,那么我们猜测的没有问题。
-
查看堆栈中的情况
-
堆栈2在断点NSKeyValueWillChange方法之后执行的
3.3 总结
- 中间类中是对属性的set方法进行了重写;
- 重写的set方法中,是对父类的属性进行赋值,并将ISA还原。
上一篇: 如何通过RunTime实现KVO?