一、KVO 初探
1.1 概念
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.
键值观察(KVO)是一种机制,允许对象在其他对象的特定属性改变后得到通知。
KVO 概念,参看 苹果开放文档
1.2 使用
注册监听
func addObserver(_ observer: NSObject,
forKeyPath keyPath: String,
options: NSKeyValueObservingOptions = [],
context: UnsafeMutableRawPointer?)实现键值变换的通知
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id> *)change
context:(void *)context;移除监听,当不再需要监听事件时
func removeObserver(_ observer: NSObject,
forKeyPath keyPath: String)
1.1 context 的作用
1.1.1 介绍
添加KVO 的关键方法为
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; |
- objserver - 观察者
- keyPath - 关键路径,即观察的重点,如属性
- options - 键值观察的策略选项。
- context - 观察上下文。
1.1.2 原因:
苹果文档 在context 这一小节 写到:
A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
更安全和扩展性更好的获取方式,就是使用
context
来确保你收到的通知被指向你的观察者,而不是父类。
可见上下文指针(context
) 是用来区分多个对象同时使用使用相同的keyPath
,这样无需区分不同对象,减少判断嵌套,提高性能。
可以快速定位观察键,是的区分观察对象更加便利、安全、直接。
类似标签区别
1.1.3 使用
context 使用静态地址指针,用法如下:
创建 context 指针
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;将不同的context 注册在相同对象的观察者中
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
1.2 移除观察者的重要性
会触发隐藏的崩溃
移除观察者,通常会使用这个方法
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; |
如果不移除,当被观察者被dealloc 后,观察者还在苦苦的观察它,长此以往,找不到对象
会形成野指针,造成崩溃。
类似女同事们默默喜欢关注吴尊好多年,结果突然你告诉我他早就脱单结婚了,女儿都八岁了,能不崩溃吗。
1.3 手动/自动观察开关
有些业务场景,经常需要变换对键的观察,由于需求的变换,可能之前一直在观察的值,这个版本就不需要在观察了,那么再去删除大量的观察代码肯定不是一件明智的事情(谁知道产品经理下一步会不会回退需求呢?)。
可以通过开关自动观察指定键的对象按钮来实现
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key; |
当需要手动来操作时,返回NO
此时对需要改动的键值,手动添加
willChangeValueForKey
和didChangeValueForKey
两个方法,这样是的被观察对象的观察策略生效。代码如下:
业务代码对对象进行观察以及改变
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person willChangeValueForKey:@"name"];
self.person.name = @"haha";
[self.person didChangeValueForKey:@"name"];
}类的实现下,关闭自动
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}当需要自动观察指定的键时,返回YES
// 自动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return YES;
}
1.4 路径集合
在某些场景下,观察对象手多个外接属性/变量影响,也可以使用KVO。
一个典型的场景,是在音乐/电视剧下载时,由于某些专辑有多首歌曲,某些时候下载过程中,会有新的关联专辑加入进来,此时下载进度条可能满了之后会会退(奇怪的需求)。
给对象添加一个下载进度的KVO 监测
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
业务层,对简洁相关联的属性,进行增减。比如这里,使用到的是下载进度相关的”已写入数据”和”总数据”,当点击时,他们会依次添加
self.person.writtenData += 10;
self.person.totalData += 20;最后,在集合关联键值方法里,给他们绑定上关系。
此时有3个关键键了,分别上:downloadProgress、totalData、writtenData
他们之间的关系是: 下载进度 = 当前下载量 / 总量 (downloadProgress = 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;
}
1.5 集合数组观察
比如对某个数组添加观察。
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
对数组进行改动。此时需要将其先改成可变数组,才能改动
[[self.person mutableArrayValueForKey: @"dateArray"] addObject: @"hello"];
原因:因为数组没有遵循KVC,并没有使用setter 方法
1.6 观察类型枚举
- NSKeyValueChangeSetting
- NSKeyValueChangeInsertion
- NSKeyValueChangeRemoval
- NSKeyValueChangeReplacement
二、KVO 原理
2.1 概念
Automatic key-value observing is implemented using a technique called isa-swizzling.
自动KVO 使用isa 改变的技术来实现
2.2 动态生成子类
为了分析所谓的动态生成子类,先写一段代码,生成一个Dog
的类,对其name
属性添加观察,并打印实例d
观察前和观察后的类名。代码如下:
Dog *d = [[Dog alloc] init]; |
运行,打印结果如下:
如何判断是子类呢,可以写个方法来打印当前Dog
类的子类列表
// 注册类的总数 |
打印结果如下:
2020-05-07 16:54:32.770527+0800 MyKVO[8567:500364] 添加前-: Dog |
果然生成了一个NSKVONotifying_Dog
基于Dog
类的子类
当对某个对象实用KVO,会动态生成子类 NSKeyValueNotifying__XXX,这个类的isa 指向对象的类
2.3 键值观察的是setter方法
我们知道KVO 是对属性做的键值观察,那么为什么不是成员变量呢?为了解决这个问题,不妨生成一个成员变量,对他做一下检测试试。
先给类
Dog
新增一个成员变量age
,记得添加@public
, 这样才能在外部访问@interface Dog : NSObject
{
@public
NSString *age;
}
@property (copy, nonatomic) NSString *petName;接下来给狗狗对象
d
添加属性petName
、成员变量age
的KVO方法[self.d addObserver:self forKeyPath:@"petName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.d addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew) context:NULL];添加一个改变属性的点击事件。当点击屏幕,给d 的属性和成员变量赋值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.d.petName = @"Jacob";
NSLog(@"self.petName-%@", self.d.petName);
self.d->age = @"18";
NSLog(@"self.age-%@", self.d->age);
}运行,点击屏幕,监控键值变化如下:
可见打印结果中,self.petName
的值变化,得到了打印验证;而 self.d->age
作为公开出来的成员变量,并不会在其值发生改变打印。为什么呢?
我们都知道属性创建之后,系统会自动配置setter 方法和getter 方法;
而成员变量,则是需要手动添加setter/getter 方法,区别就在setter 方法未实现。
所以我们得知,KVO 监控的是变量的setter 方法
2.4 动态子类会重写方法
在上文得知,会动态生成NSKeyValueNotifying__XXX
的子类后,不免想要看看这个新的类,究竟实现了那些内容。
先看看类里面有哪些方法,写一个遍历类的方法:
|
输入这个新的类:
[self printClassAllMethod: NSClassFromString(@"NSKVONotifying_Dog")]; |
打印结果如下:
可见实现的类有如下:
- set 方法 ——setPetName
- class 方法
- dealloc 方法
- _isKVOA 键的标识
2.5 KVO移除时操作
当对一个对象的监控结束后,会移除KVO 监控,这时候会发生什么呢?
2.5.1 isa 指回原类
试验一下,在dealloc
方法,添加移除方法,并在前后打印类的名字
NSLog(@"移除前,类名为: %s", object_getClassName(self.d)); |
在这里可以清晰的看到,当移除KVO 观测属性值后,这个动态子类,又变回了Dog
类,即isa 指向又从动态子类,指向了原来的类
2.5.2 动态子类会缓存
那么移除KVO 后,动态子类是否销毁?
打印一下当前d
的类的子类即可,先定义一个打印方法
- (void)printClasses:(Class)cls{ |
执行对self.d
的打印
- (void)dealloc{ |
得到结果如下:
很显然,这里的NSKVONotifying_Dog, 依旧存在,我们可以大胆推断,这里应该是缓存下来,方便下次再进行调用。
原理图如下: