KVO
KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
KVO底层实现原理分析与验证
基本使用
打印结果
那么给person1和person2赋值的时候,为什么只是添加了监听者对象的属性变化会调用呢?
我们看到person1与person2的区别就是person2添加了监听者。所以猜测是person2实例对象发生了变化。
打印结果显示person1与person2实例对象的isa指向的类对象是不同的,也就是添加了KVO监听的实例对象的isa指向的类对象是NSKVONotifying_BYPerson,未添加监听者的实例对象的isa指向的还是原来的类对象。
我们都知道实例对象调用方法是给这个对象发送消息,根据实例对象的isa,找到类对象,优先在这个类对象中去寻找对象方法,找不到的话会根据superClass指针找父类的类对象,再去父类的类对象中查询对象方法。
person1的isa
person2的isa
备注: NSKVONotifying_BYPerson这个派生的子类,是runtime动态生成的, NSKVONotifying_BYPerson这个类对象的isa指针指向这个派生类的元类对象
我们来验证下NSKVONotifying_BYPerson类中有哪些对象方法
// 打印类中的方法
- (void)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);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
NSKVONotifying_BYPerson类中有setAge:, class, dealloc, _isKVOA方法
派生的子类中的 setAge: 方法
根据结果来看person2会优先调用NSKVONotifying_BYPerson这个类对象中的setAge:方法
我们来看下NSKVONotifying_BYPerson对象方法的setAge:方法的实现:
**methodForSelector:**方法是返回实例对象对应的对象方法的实现
我们看到person1的"setAge:"方法实现没有发生变化,person2在添加监听者之前和之后是不同的
p (IMP) 地址
会打印出该方法的实现
person1是正常调用的BYPerson类的setAge:方法,而person2的"setAge:"方法的实现是函数**_NSSetIntValueAndNotify**这个函数
_NSSetIntValueAndNotify内部实现
- 调用willChangeValueForKey,记录旧的值
- 调用原来的setter方法
- 调用didChangeValueForKey (该方法内部会调用observer的observeValueForKeyPath:ofObject:change:context方法)
派生的子类中的 class 方法
- (Class)class {return [BYPerson class];}
派生的子类中重写了class方法,返回的是父类的class方法。苹果为什么要这么做呢?
我认为是苹果不想公开NSKVONotifying_BYPerson这个类,屏蔽内部实现,隐藏派生子类的实现,让开发者更注重业务的开发。 即使开发者调用class方法,告诉开发者的也是BYPerson,是父类,也是正确的。
KVOController源码讨论
KVO现状
-
addObserver和removeObserver必须配对出现,仅支持以下用法
[observe addObserver...]; // do something [observe removeObserver...]; -
很多时候明明代码已经做到了配对出现且按次序调用,由于多线程导致软件运行时环境复杂化 或者 观察者意外地过早释放掉还是发现有相关的crash。
-
对同一个实例对象的属性添加多次监听,在属性发生变化的时候,observeValueForKeyPath:ofObject:change:context的方法会调用多次
KVOController的优势
- 提供更易用的的接口
提供对block回调和自定义selector的支持,保持了灵活性和强大
- 提供更加安全的接口
通过解决addObserver 和 removeObserver必须配对且按次序调用的问题,提供了一套更加安全的接口。
使用单例主动监听,保证在程序声明周期不会释放掉。
- 消息过滤 通过_FBKVOInfo *existingInfo = [infos member:info], 首先判断是否已经包含对应观察者消息,如果包含则直接返回,不会再重复注册。
KVOController 流程
关键类
_FBKVOInfo:
{
@public
// 对FBKVOController的弱引用
__weak FBKVOController *_controller;
// 被监听者的keyPath
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
// 被监听到的实例变量发生变化之后的回调
FBKVONotificationBlock _block;
// 本对象的状态
_FBKVOInfoState _state;
}
FBKVOController:
// 用来记录监听的table
NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
// 弱引用一个被监听的对象
@property (nullable, nonatomic, weak, readonly) id observer;
_FBKVOSharedController:
// 记录监听的集合
NSHashTable<_FBKVOInfo *> *_infos;
第一步、(基本使用)
// Arrange 1: Create a controller to observe changes to a circle.
id<FBKVOTestObserving> observer = mockProtocol(@protocol(FBKVOTestObserving));
FBKVOController *controller = [FBKVOController controllerWithObserver:observer];
// Arrange 2: Observe the key paths "radius" and "borderWidth" on the circle.
// Aggregate the new values in an array.
NSMutableArray *newValues = [NSMutableArray array];
FBKVOTestCircle *circle = [FBKVOTestCircle circle];
// 将被监听者、keyPath、传到FBKVOController内部
[controller observe:circle keyPaths:@[radius, borderWidth] options:NSKeyValueObservingOptionNew block:^(id observer, FBKVOTestCircle *circle, NSDictionary *change) {
[newValues addObject:change[NSKeyValueChangeNewKey]];
}];
第二步、(在FBKVOController的操作)
-
根据FBKVOController、keyPath等创建_FBKVOInfo对象,并把被监听者作为key,创建对应NSMutableSet作为value,将_FBKVOInfo对象添加到这个NSMutableSet中。将key与value的对应关系存储到FBKVOController的的objectInfosMap(NSMapTable类型)成员变量中
-
等下次添加的时候,根据被监听者(key)判断是否创建了这个NSMutableSet(value),未创建则重新创建;如果判断已经创建了,取出对应的NSMutableSet, 再判断这个NSMutableSet是否包含了该keypath创建_FBKVOInfo对象,如果包含则直接return,未创建则重新创建。过滤掉重复监听同一个被监听者的同一个属性。
-
将被监听者和生成的_FBKVOInfo对象传给_FBKVOSharedController这个单例
第三步、(在_FBKVOSharedController中的操作)
-
让_FBKVOSharedController这个单例对被监听者进行监听,因为监听者是单例,所以监听者在程序的运行声明周期不会释放掉。
-
在监听回调方法observeValueForKeyPath中做了以下判断
2.1. 取出对应的info,判断info持有的_controller是否释放掉了,如果释放掉了直接return
2.2. 判断_controller持有的被监听者是否释放掉了,如果释放掉了直接return
-
走完上述两步,进行block的回调。保证在回调过程中被监听者、监听者和KVOController都是存在的