KVO 使用
基础使用(三步)
1、添加观察者事件
objc
[self.person addObserver:self forKeyPath:@"isImageLoadFinshed" options:(NSKeyValueObservingOptionNew) context:NULL];
context 是标签,用于区分,比如继承关系(student、person),当 keyPath 都相同时,只能通过标签区分,否则就传NULL
2、实现观察者响应方法
objc
//object是被监听的对象
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"isImageLoadFinshed"]) {
NSLog(@"change = %@", change);
}
}
- dealloc中移除监听,不移除会引发崩溃
objectivec
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"isImageLoadFinshed"];
}
高级用法
监听集合变化
在监听集合时比较特殊,在增加或减少数组元素时,以正常的方式addObject
或removeObject
无法成功监听,要使用下面这种方式
objc
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"Hello"];
为什么呢,看 KVO 的回调方法
objc
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
其中的 change 参数是传入的变化量,通过在注册时用 options 参数进行的配置,会包含不同的内容,其中NSKeyValueChangeKey
其实是NSKeyValueChange
枚举,
objectivec
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,//属性的值被重新设置
NSKeyValueChangeInsertion = 2,//表示更改的是集合属性,代表插入操作。
NSKeyValueChangeRemoval = 3,//表示更改的是集合属性,代表删除操作。
NSKeyValueChangeReplacement = 4,//表示更改的是集合属性,代表替换操作。
};
我们大部分数据类型可以通过setter
方法监听变化,即值变化的类型是NSKeyValueChangeSetting
,而数组等集合类型不是这样的,addObject
并不会调用setter
方法,因此需要通过 KVC 的mutableArrayValueForKey:
获取到数组的代理对象,再进行增删改,才可以监听到数组内容的变化。
猜测
mutableArrayValueForKey
底层是先获取到数组,进行增删改,然后再把修改完的数组 set 回去,触发的 KVO 监听。
联动观察
只要监听一个属性值,就可以联动观察其他属性值的变化
场景一:
比如有个Student
类如下:
less
@interface Student : NSObject
@property (nonatomic , assign) NSInteger englishScore; // 英语分数
@property (nonatomic , assign) NSInteger mathScore; // 数学分数
@property (nonatomic , assign) NSInteger chineseScore; // 语文分数
@end
一个学生的总分=英语分数+数学分数+语文分数,三门学科中任意一个分数发生变化总分都会跟着变。如果有个界面只需要展示总分,我们可以通过KVO
监听englishScore
、mathScore
、chineseScore
这三个属性的变化来实现我们的需求。那如果有 10 门学科,那就得监听 10 门学科分数的变化,这样太麻烦了,那可不可以只监听totalScore
这一关属性就达到相同的效果呢?当然是可以的,这里就需要用到联动观察
。
首先给Student
类增加个totalScore
属性,重写keyPathsForValuesAffectingValueForKey
方法来设置与这个属性相关联的属性(也就是哪些属性值的改变会影响totalScore
的改变),为了方便使用,这里也重写它的getter
方法,代码如下
objc
#import "Student.h"
@implementation Student
// 重写getter方法,也就是如何通过其他属性来计算totalScore的值
- (NSInteger)totalScore{
return _mathScore + _englishScore + _chineseScore;
}
// 设置关联属性(重写keyPathsForValuesAffectingValueForKey方法)
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSMutableSet *set = [[NSMutableSet alloc] initWithSet:[super keyPathsForValuesAffectingValueForKey:key]];
if ([@"totalScore" isEqualToString:key]) {
// affectKeys是所有关联属性的属性名
NSArray *affectKeys = @[@"englishScore",@"mathScore",@"chineseScore"];
[set addObjectsFromArray:affectKeys];
}
return set;
}
@end
场景二:
Stedent
类有name
和age
两个属性,School
类有个Student
类型的属性,现在要实现Stedent
类的任意一个属性的改变都会触发KVO
,那我们可以通过KVO
同时监听Student
实例对象的2个属性变化,这和上面一样,当Student
类的属性有很多时就会比较麻烦,因为要同时监听很多属性的变化。其实可以和上面一样,设置好相关的 keypath 后,我们只需要监听School
类中的student
这一个属性就可以了。代码如下:
objc
// Student.h
@interface Student : NSObject
@property (nonatomic , copy) NSString *name; // 姓名
@property (nonatomic , assign) NSInteger age; // 年龄
@end
// Student.m
#import "Student.h"
@implementation Student
@end
// School.h
#import "Student.h"
@interface School : NSObject
@property (nonatomic , strong) Student *student;
@end
// School.m
#import "School.h"
@implementation School
// 关键就是这里设置相关的keyPath
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSMutableSet *set = [[NSMutableSet alloc] initWithSet:[super keyPathsForValuesAffectingValueForKey:key]];
if ([@"student" isEqualToString:key]) {
NSArray *affectKeys = @[@"_student.name",@"_student.age"];
[set addObjectsFromArray:affectKeys];
}
return set;
}
@end
禁止监听
禁止目标类所有属性的监听,就把automaticallyNotifiesObserversOfSteps
返回NO
。
objc
@implementation LGPerson
+ (BOOL)automaticallyNotifiesObserversOfSteps {
return NO;
}
@end
禁止目标类某个属性的监听,下面代码意思是禁止外部对 name 属性的监听,不影响其他属性的监听
objc
@implementation LGPerson
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return YES;
}
@end
手动观察
手动观察的前提是属性被禁止监听,需要用到两个方法willChangeValueForKey:
和didChangeValueForKey:
objectivec
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO;
}
return YES;
}
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
KVO底层原理
KVO 的底层实现大致可以描述如下:
- 动态创建子类 NSKVONotifying_XXX,
- 创建 class 方法,class 指向原类,为了隐藏这个子类
- 创建 setter 方法,内部把消息转发给父类,并触发回调 observeValueForKeyPath
- 创建 dealloc 方法
- 创建 isKVOA
- 处理 removeObserver 操作
- isa 指回父类
- 不会移除这个子类,即把这个子类缓存下来,为防止下次监听时再次创建耗费性能,这里用空间换时间
FBKVOController源码解析
FBKVOController优点
- 写法简洁:增加了 block 回调、自定义回调或
NSKeyValueObserving
回调通知 - 不需要手动 removeObserver(系统 KVO 如果 remove 2 次会 crash),会在观察者被释放时自动释放
架构设计
FBKVOController
持有NSMapTable
,以object
为key
得到相对应的NSMutableSet
。NSMutableSet
中存储了不同的_FBKVOInfo
。这套数据结构的主要作用是防止开发人员重复添加相同的KVO
。当检查到其中已存在相同的_FBKVOInfo
对象时,不再执行后面的代码。_FBKVOSharedController
持有NSHashTable
。NSHashTable
以弱引用的方式持有不同的_FBKVOInfo
。此处实际执行KVO
代码。_FBKVOInfo
有一个重要的成员变量_FBKVOInfoState
,根据这个枚举值(_FBKVOInfoStateInitial
、_FBKVOInfoStateObserving
、_FBKVOInfoStateNotObserving
) 来决定新增或者删除KVO
。
被监听对象和属性的关系
使用 NSMapTable 保存了被监听对象 object 和 FBKVOInfo 对象集合的关系。
源码学习收获
1 中介者模式
中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。
比如我们平时买房、租房都会找中介,通过中介可以更快更高效的找到合适的房子,很多事情中介帮我们去做了,不用我们自己去找房源,如图
中介者模式 4 种角色:
- 抽象中介者(Mediator)角色: 抽象中介者角色定义统一的接口,以及一个或者多个事件方法,用于各同事角色之间的通信。
- 具体中介者(ConcreteMediator)角色: 实现了抽象中介者所声明的事件方法,协调各同事类之间的行为,持有所有同事类对象的引用。
- 抽象同事(Colleague)角色: 定义了抽象同事类,持有抽象中介者对象的引用。
- 具体同事(ConcreteColleague)角色: 继承抽象同事类,实现自己业务,通过中介者跟其他同事类进行通信。
举个例子:有 ViewController1、ViewController2 要监听 Person1(name、age)、Person2(name、age),使用FBKVOController
后,各自扮演的角色
FBKVOController
和FBKVOSharedController
是中介类,可以理解前者为抽象中介者,定义了所有的接口方法(observe、unObserve...);后者为具体中介者,实现了抽象中介者声明的具体方法,有序的交给系统 KVO 处理监听任务;ViewController1
和ViewController2
是同事类,它们通过中介者FBKVOController
监听某对象的某属性,由系统 KVO 处理完后,再经过中介者把回调传递给自己。
2 NSMapTable和NSHashTable
NSMapTable
NSMapTable
是NSMutableDictionary
的高阶类型,可以给 key 和 value 设置NSPointerFunctionsOptions
,传统字典就相当于把它的 keyOptions 设成这样:
objc
NSPointerFunctionsStrongMemory |
NSPointerFunctionsObjectPersonality|
NSPointerFunctionsCopyIn;
需要注意的是NSPointerFunctionsCopyIn
,老字典会对 key 进行 copy,value 不会,但是如果大家平日里都使用NSString
作为 key,那大可不必考虑 copy 的性能损耗(因为只是浅拷贝)。但如果使用的是NSMutableString
或者一些进行深拷贝的类型,那就另当别论了。
再把它的 valueOptions 设成这样:
objc
NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality
NSMapTable
与老字典的性能不能一概而论,因为他们的主要性能差别也是来自于NSPointerFunctionsCopyIn
与NSPointerFunctionsWeakMemory
。后者会带来一定的性能损耗,而前者要看 key 的NSCopying
协议是如何实现的。
NSHashTable
NSHashTable
是NSMutableSet
的高阶类型,与NSPointerArray
、NSMapTable
一样,可以指定NSPointerFunctionsOptions
,只有可变版本,允许对添加到容器中的对象是弱引用的持有关系, 当NSHashTable
中的对象销毁时,该对象也会从容器中移除。
但是添加对象的时候前者耗费的时间是后者 2 倍,所以平时能用NSSet
就用NSSet
。
3 锁
4 == 和 isEqual 的关系
==
对于基本类型,比较的是值;对于对象类型, 比较的是对象的地址(即是否为同一对象)。isEqual
比较的是两个对象是否相同,对于系统对象比如 UIColor,系统内部已经实现了isEqual
方法,举例如下:
objc
UIColor *color1 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
UIColor *color2 = [UIColor colorWithRed:0.5 green:0.5 blue:0.5 alpha:1.0];
NSLog(@"color1 == color2 = %@", color1 == color2 ? @"YES" : @"NO");
NSLog(@"[color1 isEqual:color2] = %@", [color1 isEqual:color2] ? @"YES" : @"NO");
结果如下
ini
color1 == color2 = NO
[color1 isEqual:color2] = YES
这个例子表示,UIColor 对象的isEqual
方法实现的是两个对象的 color 是否相同,实际上这是 2 个不同的对象,从==
结果可以看出来。
5 isEqual和hash关系
1、FBKVOInfo 为什么重写 isEqual 方法和 hash?
这个自定义对象会在 FBKVOSharedController 中被添加到 NSHashTable,hash table 会根据 hash 值优化索引效率,基于 hash 值索引的 Hash Table(NSSet、NSDictionry 也是) 查找某个成员的过程是:
- 通过 hash 值直接找到查找目标的位置
- 如果目标位置上有多个相同 hash 值得成员,此时再按照数组方式遍历判等(
isEqual
)
2、hash 什么时候会被调用?
NSSet 添加新成员时, 需要根据 hash 值来快速查找成员, 以保证集合中是否已经存在该成员,NSDictionary 在查找 key 时, 也利用了 key 的 hash 值来提高查找的效率。
3、如何重写 hash 方法?
对关键属性的位或运算作为 hash 值,参考如下:
objc
- (NSUInteger)hash {
return [self.name hash] ^ [self.age hash];
}