在一些场景中我们有时候会用到KVO,KVO的原理是什么样子的呢,如果自己定义一个KVO要怎么做呢?
KVO
Key-value observing provides a mechanism that allows objects to be notified of changes to specific properties of other objects.
KVO提供了一种机制,允许在其他对象的特定属性发生变化时通知对象。
通常我们使用KVO时需要以下三个步骤:
- 使用
addObserver:forKeyPath:options:context:
向被观察对象注册观察者。 - 实现
observeValueForKeyPath:ofObject:change:context:
这个方法来接受变更通知消息。 - 当观察者不再接受消息时,使用
removeObserver:forKeyPath:
方法注销观察者。至少,在从内存中释放观察者之前调用这个方法。
context
在addObserver:forKeyPath:options:context:
中context
在有多个对象的属性同时被监听时,我们可以申明不同的context来识别
eg:
关联变化keyPathsForValuesAffectingValueForKey
之前有做过一个图片管理器,里面显示的上传进度,但是在图片上传的时候,也会有别的图片在这个时刻被选择,然后加入到总的上传图片的队列里面,我们这个时候可以用如下方法监听
downloadProgress
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
这个时候你改变了totalData
和writtenData
,downloadProgress
都可以被监听到。
可变数组添加变量的时候监听
eg1:
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
当我们使用[self.person.dateArray addObject:@"1"];
我们是无法监听到dateArray
的变化的,要使用[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
removeObserver:forKeyPath:
一定要手动移除吗?
我们知道iOS9之后,对于普通的添加观察者的方法不需要手动移除self。
下面我们看一种情况:
在detailViewController中,有一个student的单例。我们给它添加观察者,这个页面消失的时候,由于student仍有之前的观察者,当多次进到详情页面,点击屏幕的时候,都会对观察者发送改变,而有的这个时候已经被内存回收了,就会出现EXC_BAD_ACCESS
异常。
手动开关
在某些情况下,你可能希望控制通知流程。
对于我们想要通知的属性我们可以使用
automaticallyNotifiesObserversForKey
返回NO
。
要实现手动观察者通知,可以调用willChangeValueForKey
和didChangeValueForKey
这两个方法
KVO的实现原理
主要实例代码:
//LGPerson.h
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
//Person.m
@implementation Person
- (void)setNickName:(NSString *)nickName{
_nickName = nickName;
}
//Student.h
#import "Person.h"
@interface Student : Person
@end
//Student.m
#import "Student.h"
@implementation Student
- (void)setNickName:(NSString *)nickName{
}
@end
//ViewController.m
self.person = [[Person alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
由上面知道,观察者是使用isa-swizzling技术实现的。当观察者为一个对象的属性注册时,被观察对象的isa指针被修改,指向了一个中间类而不是真正的类。因此,isa指针的值不一定能反映实例的实际类。
- ❓中间类里面做了什么呢
- ❓中间类与原类有什么关系呢
- ❓中间类是什么时候被销毁的呢 带着这些疑问我们来探究KVO的原理。
动态子类
我们在
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
前后打印self.person发现在添加完观察者之后,self.person指向了一个NSKVONotifying_LGPerson
中间类。
与原类的关系
我们知道,在添加了观察者之后,元类的isa指针指向了一个NSKVONotifying_LGPerson
这个类,
我们在添加观察者前后打印类的子类信息,打印方法如下
- (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);
}
打印信息如下
NSKVONotifying_Person
为Person
的子类。
重写方法
中间类NSKVONotifying_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);
}
打印入上图所示,在
NSKVONotifying_Person
中重写了
setNickName
class
dealloc
_isKVOA
这四个方法。
移除观察者的时候做了什么
从打印的数据看出,在移除观察的时候,isa指针又重新指回了过来。
NSKVONotifying_Person
有销毁吗?
我们在前一个页面,打印一下Person的类信息,如下图
说明中间类没有销毁。
在添加了观察者之后,系统会自动生成一个动态子类,原类的isa指针会指向动态子类,动态子类与原类是父子关系,在动态子类中重写了setter、class、dealloc、_isKVOA这四个方法,在移除观察者的时候,isa的指针会重新指回来。