一、KVO概念
上回分析了KVC的原理,我们知道了KVC是闭源的,唯一能看的就是官方文档,那么这回KVO也是一样,请您参考KVO官方文档。
根据官方文档的描述,KVO全称Key-value observing
,键值观察 -->允许在其他对象的指定属性发生更改时通知对象。同时,值得我们注意的是,官网明确指出In order to understand key-value observing, you must first understand key-value coding.
,意思就是在理解KVO之前,必须先理解KVC。
二、KVO基本用法
先声明Model类LGPerson
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
// 调用的地方: 例如ViewController.m中
@interface ViewController ()
@property (nonatomic, strong) LGPerson *person;
@end
大致分为以下几步:
- 添加观察者
- (void)viewDidLoad {
[super viewDidLoad];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- 实现KVO回调
observeValueForKeyPath:ofObject:change:context
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"%@",change);
}
}
- 移除观察者
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}
2.1 参数context的作用
KVO中的涉及了几个参数,例如observer
【观察者】keyPath
【观察的属性值名称或路径】和change
【观察对象的值的一个集合,是个字典,根据key-->NSKeyValueObservingOptions可查询到旧值和新值等其它】。
但是,最后一个参数context
比较难以理解,官网描述是这样的👇
翻译:
addObserver:forKeyPath:options:context:
方法中的上下文context
指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。可以通过指定context
为NULL,从而依靠keyPath
即键路径字符串传来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径而导致问题。所以可以为每个观察到的keyPath创建一个不同的context
,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析。
一般的使用过程中,context上下文
主要是用于区分不同对象的同名属性。
注意图中红框处可知,在KVO回调方法中可以直接使用context
进行区分,可以大大提升性能,以及代码的可读性。
示例:
再声明类LGProduct
,也有一个属性name
:
@interface LGProduct : NSObject
@property (nonatomic, copy) NSString *name;
@end
然后在ViewController.m
中也添加观察
static void *PersonNameContext = &PersonNameContext;
static void *ProductNameContext = &ProductNameContext;
@interface ViewController ()
@property (nonatomic, strong) LGPerson *person;
@property (nonatomic, strong) LGProduct *product;
@end
@implementation ViewController
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
[self.product removeObserver:self forKeyPath:@"name"];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
self.product = [[LGProduct alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self.product addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:ProductNameContext];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person setName:@"personName"];
[self.product setName:@"productName"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNameContext) {
NSLog(@"%@",change);
}else if (context == ProductNameContext){
NSLog(@"%@",change);
}
}
@end
运行结果👇
2.2 移除观察者
是否需要移除观察者?我们可以不移除,看看会报什么bug?
声明类LGStudent
,继承LGPerson
,定义单例方法👇
@interface LGStudent : LGPerson
+ (instancetype)sharedInstance;
@end
@implementation LGStudent
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static LGStudent *instance;
dispatch_once(&onceToken, ^{
instance = [[LGStudent alloc] init];
});
return instance;
}
@end
新建一个页面ViewController2
,然后在ViewController
中添加button
,点击事件push到ViewController2
。
@interface ViewController2 ()
@property (nonatomic, strong) LGPerson *person;
@end
@implementation ViewController2
- (void)dealloc {
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
self.student = [LGStudent sharedInstance];
[self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.student.name = @"hello, world!";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"ViewController2 %@",change);
}
@end
当前页面流程是v1-->v2,vc2中不移除,运行👇
崩溃是因为,由于第一次添加KVO观察者后没有移除,再次进入界面,因为
student是单例对象
,接着第二次添加KVO观察者,导致KVO观察的重复添加,而且第一次的通知对象还在内存中没有进行释放,此时接收到属性值变化的通知,系统只能找到现有的通知对象,即第二次KVO注册的观察者,第一次的找不到,所以导致了类似野指针的崩溃,即内存中一直有一个野通知,且一直在监听。并且,这种crash很难定位,上图可知,这种crash根本没日志查看原因。
2.3 手动和自动开关
根据官方文档,我们发现,有个BOOL返回值方法,可以控制KVC的监听是自动还是手动,就类似一个开关👇
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
返回NO时,就不会触发监听通知。如果在NO时,也想监听属性值的变化,则必须切换为手动
👇
- (void)setName:(NSString *)name{
//手动开关
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
2.4 路径处理
2.4.1 一对一情况
更改LGStudent
@interface LGStudent : LGPerson
+ (instancetype)sharedInstance;
@property (nonatomic, copy) NSString *fullName;
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end
@implementation LGStudent
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static LGStudent *instance;
dispatch_once(&onceToken, ^{
instance = [[LGStudent alloc] init];
});
return instance;
}
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",self.firstName, self.lastName];
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
@end
调用的ViewController2
@interface ViewController2 ()
@property (nonatomic, strong) LGStudent *student;
@end
@implementation ViewController2
- (void)dealloc {
// [self.student removeObserver:self forKeyPath:@"name"];
[self.student removeObserver:self forKeyPath:@"fullName"];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
self.student = [LGStudent sharedInstance];
// [self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.student addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.student.name = @"hello, world!";
self.student.firstName = @"firstName";
self.student.lastName = @"lastName";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"ViewController2 %@",change);
}
2.4.2 一对多情况
修改LGProduct
@interface LGProduct : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) CGFloat currentData;
@property (nonatomic, assign) CGFloat totalData;
@property (nonatomic, copy) NSString *currentProcess;
@end
@implementation LGProduct
// 合二为一的观察方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"currentProcess"]) {
NSArray *affectingKeys = @[@"totalData", @"currentData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- (NSString *)currentProcess{
if (self.currentData == 0) {
self.currentData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.currentData/self.totalData];
}
@end
ViewController
中监听
@interface ViewController ()
@property (nonatomic, strong) LGProduct *product;
@end
@implementation ViewController
- (void)dealloc {
[self.product removeObserver:self forKeyPath:@"currentProcess"];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.product = [[LGProduct alloc] init];
[self.product addObserver:self forKeyPath:@"currentProcess" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.product.currentData += 10;
self.product.totalData += 1;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
2.5 数组元素的观察
LGPerson
类中添加数组属性
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSMutableArray *dateArray;
@end
接着在ViewController
中监听dateArray
@interface ViewController ()
@property (nonatomic, strong) LGPerson *person;
@end
@implementation ViewController
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"dateArray"];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
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.dateArray addObject:@"1"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
上述示例证明:可变数组如果直接添加数据,是不会调用setter方法的,直接通过[self.person.dateArray addObject:@"1"];
向数组添加元素,是不会触发KVO通知回调的。
由于KVO是基于KVC的,我们去KVC官方文档上,查查对类似可变数组的集合的键值,是通过什么方法添加元素的?
于是修改添加元素的方式
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// [self.person.dateArray addObject:@"1"];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
运行后结果👇
发现kind
值为2,之前的都是1。我们再看看这个kind
是什么意思?根据NSDictionary<NSKeyValueChangeKey,id> *change
类型,kind
是NSKeyValueChangeKey枚举
👇
由此可知,一般对象类型的属性与集合类的属性,它们的KVO观察是有区别的,其kind值不同
- 一般对象类型的属性的kind一般是设值
NSKeyValueChangeSetting = 1
- 集合类型的属性kind是插入
NSKeyValueChangeInsertion = 2
三、KVO底层大致原理
还是看官网文档描述
翻译过来,大致有以下几点:
- KVO是使用
isa-swizzling
的技术实现的。 - isa指针指向维护分配表的对象的类。该表实质上包含指向该类实现的方法的指针以及其他数据。
- 当为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。
- isa指针的值不一定指向实例的实际类。
- 不应该依靠isa指针来确定类的成员身份。相反,应该使用class方法来确定对象实例的类。
3.1 观察的对象
上述文档说明是针对属性的观察,真的如此吗?我们先用代码验证一下:
首先在类LGPerson中
添加成员变量kcName
👇
@interface LGPerson : NSObject {
@public
NSString *kcName;
}
@property (nonatomic, copy) NSString *name;
@end
然后在ViewController
中观察kcName
和 name
👇
static void *PersonNameContext = &PersonNameContext;
static void *PersonKCNameContext = &PersonKCNameContext;
@interface ViewController ()
@property (nonatomic, strong) LGPerson *person;
@end
@implementation ViewController
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
[self.person addObserver:self forKeyPath:@"KcName" options:NSKeyValueObservingOptionNew context:PersonKCNameContext];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = @"";
self.person->kcName = @"";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (context == PersonNameContext) {
NSLog(@"LGPerson name %@",change);
}else if (context == PersonKCNameContext){
NSLog(@"LGPerson KcName %@",change);
}
}
@end
运行👇
上图可知,只观察到了name
,而KcName
却没有。
属性与成员变量的区别是什么呢?就是set & get方法,很明显,KVO观察的对象就是属性的set方法。
3.2 中间类
当为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类
。这个中间类
是什么呢?有哪些成员变量,属性和方法呢?
带着上述问题,我们尝试用代码验证:
在添加观察者的地方打断点
观察之前是
LGPerson
,观察之后是NSKVONotifying_LGPerson
,类发生了变化,实质就是isa指针指向发生了变化
。接着,
NSKVONotifying_LGPerson
这个类与LGPerson
有没有关系呢,是继承关系还是没有关系?我们可以利用runtime的api打印类的继承链,看看👇
#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);
}
//********调用********
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//********调用********
[self printClasses:[LGPerson class]];
}
上图可知,NSKVONotifying_LGPerson是LGPerson的子类
。
至此,我们知道了中间类
是原被观察对象类的派生类
,二者是继承关系
。那中间类
里有什么属性,成员变量或方法呢?同样的,我们可以利用runtime的api打印看看👇
#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);
}
//********调用********
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//********调用********
[self printClasses:[LGPerson class]];
[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
}
上图可知,有4个方法,分别是
setName
、 class
、 dealloc
、 _isKVOA
。
-
dealloc
-->析构
。 -
_isKVOA
--> 判断是否中间类
。 -
class
-->当前类。 -
setName
方法。
多了一个setName方法
,LGPerson
中同样也有一个,那么问题来了,这个set方法是继承的,还是重写的
?一样,我们可以用代码验证👇
首先在LGPerson
的setName
中打印日志,并断点
@implementation LGPerson
- (void)setName:(NSString *)name {
NSLog(@"LGPerson %s", __func__);
}
@end
然后先注释KVO观察
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
// [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.person.name = @"123";
}
运行👇
再添加KVO观察
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
中间过程多了3步,注意是
NSObject(NSKeyValueObservingPrivate)
这个类发出的消息,NSObject
的KVO分类,同时查看当前self的类信息👇是中间类NSKVONotifying_LGPerson
,这就说明中间类
的setName
方法是重写,而非继承。
那set方法里有什么重要的细节? set方法是中间类
的,但最终是改变的是LGPerson类属性name的值
,那么这个过程中间肯定有一个传值的过程,我们看看是在哪里?通过断点栈的查找发现👇
是在NSKeyValueWillChange
和 NSKeyValueDidChange
之间。
再来看看,在移除观察后,中间类
是否会被移除,被系统回收呢?
我们可以在ViewController2
中的dealloc
中移除观察👇
在ViewController
中打印子类信息👇
运行打印查看👇
发现没有移除,类似于缓存的原理,不移除就是下次进来时方便添加观察者,不需要每次去内存申请新的空间,节省了开销。
最后我们看看isa指针是否会指回来,何时发生?
在dealloc里移除观察者的地方打断点,查看👇
发现是在移除观察之后指回来的。
3.3 KVO原理总结
- KVO观察的是属性的
setter方法
,如果是集合类型的属性
,对应的kind
的值为NSKeyValueChangeInsertion = 2
。 -
中间类
是原被观察对象类的派生类
,即原类的isa指针指向了中间类
。 -
中间类
会重写setter方法
,重写调用完成后,会改变原类的属性值。 - 在移除观察时,
中间类
不会被移除,只是原类的isa指针指向会指回来。
自定义KVC
简单的实现了一个自定义函数式KVO,自动销毁监听,不需显示移除👇
完整demo
网友评论