iOS 底层 KVO - AlvinSunny/OC-TheUnderlying GitHub Wiki
KVO的全称是 Key Value Observing, 俗称“键值监听/观察”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变,当被观察对象属性值改变时,会触发KVO的监听方法来通知观察者
KVO和NSNotification都是iOS中观察者模式的一种运用
KVO可以监听单个属性的变化,也可以监听集合对象的变化,监听集合对象变化时需要通过KVC的mutableArrayValueForkey:、mutableSetValueForKey:等可变代理方法获取集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,就会触发KVO的监听方法,集合对象包含NSArray、NSSet、NSDictionary等
KVO使用三部曲: 添加/注册KVO监听、实现监听方法(用来接收属性改变通知)、移除KVO监听
1.调用方法addObserver:forKeyPath:options:context:给被观察对象添加观察者
2.在观察者类中实现 observerValueForKeyPath:ofObject:change:context:方法以接收属性改变的通知
3.当观察者不在需要监听时,调用removeObserver:forKeyPath:方法将观察者移除;需要注意的是:至少需要在观察者销毁之前调用此方法,否则可能会导致Crash
注册方法的理解
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
使用
[self.persion addObserver:self forKeyPath:NSStringFromSelector(@selector(myList)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
- target: 被观察者
- observer: 观察者
- keyPath: 被观察对象的属性的关键路径,不能为nil
- options: 观察的配置选项,包括观察的内容(枚举类型)
NSKeyValueObservingOptionNew: 观察新值
NSKeyValueObservingOptionOld: 观察旧值
NSKeyValueObservingOptionInitial: 观察初始值,如果想在注册观察者后,立即接收一次回调 既可以加入该枚举
NSKeyValueObservingOptionPrior: 分别在值改变前后触发方法(即一次改变有两次触发:即将修改属性、修改完属性)
- context: 可以传入任意数据(任意类型的对象或者C指针),在监听方法中可以接收到这个数据,是KVO中一种传值方式;如果传的是一个对象你必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash
监听方法的理解:
观察者对象必须能响应以下监听方法,因为当被观察者属性发生改变时会调用它
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
- keyPath: 被观察对象的属性路径
- object: 被观察对象
- change: 属性值改变的详细信息,根据注册方法中
options参数传入的枚举来返回
NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key),对应枚举类型NSKeyvalueChange
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1, 属性(包括集合)进行赋值操作
NSKeyValueChangeInsertion = 2, 集合对象插入元素
NSKeyValueChangeRemoval = 3, 集合对象删除元素
NSKeyValueChangeReplacement = 4, 集合对象替换元素
};
NSKeyValueChangeNewKey: 存储新值
NSKeyValueChangeOldKey: 存储旧值
NSKeyValueChangeIndexesKey: 观察者是集合对象,且进行的是(插入、删除、替换)操作
NSKeyValueChangeNotificationIsPriorKey: 对应options传入的NSKeyValueObservingOptionPrior
context: 注册时传入的context
KVO移除
在调用注册方法后,KVO并不会对观察者进行强引用(unsafe_unretained),所以需要注意观察者的生命周期;至少需要在观察者销毁之前调用以下方法移除观察者,否则如果在观察者释放后再次触发KVO监听方法就会导致Crash
iOS9之前使用通知需要在dealloc调移除监听者的方法removeobserver:,因为ios9之前观察者注册的时候通知中心对观察者对象有一个unsafe_unretained引用(cocoa 和 cocoa touch中一些类不支持weak引用),在ios9之后已经支持了weak 所以大多数情况不需要自己再手动调用removeObserver了,但是通过addObserverForName:object:queue:usingblock注册的观察者还是需要手动释放 因为通知中心对它们做了强引用
KVO触发分为自动触发和手动触发两种方式
自动触发
-
监听对象属性值的改变时: 使用点语法赋值、使用setter方法赋值、使用KVC的setValue:forKey赋值、使用KVC的setValue:forKeyPath:赋值
-
监听集合对象的改变,需要通过KVC的mutableArrayValueForKey:等方法获得代理对象,并使用代理对象进行操作,当代理对象内部发生改变时就会触发KVO,集合对象包含NSMutableArray和NSMutableSet(注意:直接(没有获取代理)对集合对象进行操作改变,不会触发KVO)
-
直接给集合对象赋值为nil会触发KVO监听回调(因为这就是在修改集合这个属性呀), 但如果重写
automaticallyNotifiesObserversForKey方法并返回NO就无法触发了
手动触发
普通对象属性调用
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
NSMutableArray、NSMutableSet对象调用
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key
注意📢: 如果注册时options传入NSKeyValueObservingOptionPrior,那么可以通过值调用willChangeValueForKey或willChange:valuesAtIndexes:forKey来触发改变前的那次KVO,可以用于在属性值即将更改前做一些操作
使用场景:
- 修改成员变量时想让外界知道属性被修改了 [示例1]
- 新值旧值相等时不触发KVO,不相等时触发KVO [示例2]
示例1
[self willChangeValueForKey:NSStringFromSelector(@selector(name))];
_name = @"成员变量_name修改了";
[self didChangeValueForKey:NSStringFromSelector(@selector(name))];
示例2
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = YES;
if([key isEqualToString:@"name"]) {
automatic = NO;
}else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)setName:(NSString *)name {
if (![_name isEqualToString:name]) {
[self willChangeValueForKey:NSStringFromSelector(@selector(name))];
_name = @"成员变量_name修改了";
[self didChangeValueForKey:NSStringFromSelector(@selector(name))];
}
}
observationInfo属性
- observationInfo属性是NSKeyValueObserving.h文件中系统通过分类给NSObject添加的属性,所以所有继承自NSObject的对象都含有该属性
- 可以通过observationInfo属性查看被观察对象的全部观察信息,包括observation、keyPath、options、context等
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
context
context在注册时传入,在监听方法里带回其作用如下:
-
标签/区分,可以更精确的确定被观察对象属性,用于继承+多监听 也可以用来传值;KVO只有一个监听回调方法,通常情况下可以在注册方法中指定context为NULL,并在监听方法中通过object和keyPath来判断触发KVO的来源,但如果存在继承的情况大家都对一个对象的同一个属性监听了,那么监听方法被触发时谁来处理就是个问题,此时就需要在注册时为context设置不同的值,然后在监听方法中校验即可做到
-
苹果的推荐用法:用context来精确的确定被观察对象属性,使用唯一命名的静态变量地址作为值,可以为整个类设置一个context,然后在监听方法中通过object和keyPath来确定被观察属性,这样存在继承的情况就可以通过context来判断; 也可以为每个被观察对象属性设置不同的context,这样就可以精确的确定被观察对象属性
context优点:嵌套少、性能高、更安全、扩展性强
context注意点:
如果传入的是一个对象,必须在移除观察者之前持有它的强引用(观察者需要强引用传入的这个对象,这样才能保证在回调方法中这个对象是可用的),否则在监听方法中访问context就可能导致Crash;
空传NULL而不应该传nil, 因为context接收的是 void *指针不是实例对象; 附iOS中nil 、NULL、Nil 、null、NSNull
KVO监听集合对象
自动触发
- (void)viewDidLoad {
[super viewDidLoad];
self.persion = [[Persion alloc] init];
self.persion.myList = [NSMutableArray array];
[self.persion addObserver:self forKeyPath:NSStringFromSelector(@selector(myList)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//[self.persion.myList addObject:@"2"];//直接(没有获取代理)对集合对象进行操作改变,不会触发KVO
//self.persion.myList = nil;
NSMutableArray *list = [self.persion mutableArrayValueForKey:NSStringFromSelector(@selector(myList))];
[list addObject:@"1"];//会触发
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"KVO被触发");
}
手动触发
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.persion = [[Persion alloc] init];
self.persion.myList = [NSMutableArray array];
[self.persion addObserver:self forKeyPath:NSStringFromSelector(@selector(myList)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray *list = [self.persion mutableArrayValueForKey:NSStringFromSelector(@selector(myList))];
[list addObject:@"1"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"KVO被触发");
}
Persion.m
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
BOOL automatic = YES;
if([key isEqualToString:NSStringFromSelector(@selector(myList))]) {
automatic = NO;
}else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)insertMyList:(NSArray *)array atIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:NSStringFromSelector(@selector(myList))];
[self.myList insertObjects:array atIndexes:indexes];
[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:NSStringFromSelector(@selector(myList))];
}
- (void)removeMyListAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:NSStringFromSelector(@selector(myList))];
[self.myList removeObjectsAtIndexes:indexes];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:NSStringFromSelector(@selector(myList))];
}
- (void)replaceMyListAtIndexes:(NSIndexSet *)indexes withMyList:(NSArray *)array {
[self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:NSStringFromSelector(@selector(myList))];
[self.myList replaceObjectsAtIndexes:indexes withObjects:array];
[self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:NSStringFromSelector(@selector(myList))];
}
KVO的触发控制
可以在被观察对象的类中重写 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法来控制KVO的自动触发
如果我们只允许外界观察person的name属性,就可以在persion类如下操作,这样外界就只能观察name属性;即使外界注册了对persion其他属性的监听,那么在属性发生改变时也不会触发KVO; 假如想所有属性都不允许监听直接返回NO即可
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if([key isEqualToString:@"name"]) {
return YES;
}
return NO;
}
也可以实现遵循命名规则为+ (BOOL)automaticallyNotifiesObserversOf<Key>的方法来单一控制属性的KVO自动触发,为属性名(首字母大写)
+ (BOOL)automaticallyNotifiesObserversOfName {
return NO;
}
注意📢:
第一个方法的优先级高于第二个方法,如果实现了automaticallyNotifiesObserversForKey:并对Key做了处理 则系统就不会再调用该Key的automaticallyNotifiesObserversOf<Key>方法
options指定的NSKeyValueObservingOptioninitial触发的KVO通知是无法被automaticallyNotifiesObserversForKey阻止的
一对一关系
有些情况下,一个属性的改变需要依赖于另一个或多个(有限的数量比如3个)属性的改变;比如我们想要对Download类中的downloadProgress属性进行KVO监听,该属性的改变依赖于writtenData和totalData属性的改变,观察者监听了downloadProgress当writtenData和totalData属性改变时观察者也应该被通知
两种解决办法:
- 重写
keyPathsForValuesAffectingValueForKey方法来指明downloadProgress属性依赖于writtenData和totalData
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if([key isEqualToString:NSStringFromSelector(@selector(downloadProgress))]){
NSArray *affectingKeys = @[NSStringFromSelector(@selector(writtenData)),NSStringFromSelector(@selector(totalData))];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- 实现一个遵循命名规则为
keyPathsForValuesAffecting<Key>的类方法,是依赖于其他值的属性名(首字母大写)
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress {
NSArray *affectingKeys = @[NSStringFromSelector(@selector(writtenData)),NSStringFromSelector(@selector(totalData))];
return affectingKeys;
}
注意📢:
1.以上两个方法可以同时存在且都会调用,但最终结果会以keyPathsForValuesAffectingValueForKey:为准
2.不适用于集合属性
一对多关系
场景: 假如Persion类有一个数组list,数组中元素是Student类型, Student类有一个age,希望Persion有一个totalAge属性来计算所有age的和;此时totalAge就依赖list中Student的age属性
方法一: 遍历数组中所有的Student对其添加关于age属性的监听,并设置context的值为一个特殊标记(比如static void *totalAgeContext = &totalAgeContext;),然后在监听方法里处理相关逻辑
方法二: 使用iOS中观察者模式的另一个实现方式: 通知(NSNotification)
-
移除观察者的注意点,在调用KVO注册方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,至少需要在观察者销毁之前移除观察者,否则如果在观察者被释放后再次触发KVO监听方法就会导致Crash
-
KVO的注册方法和移除方法应该是成对的,如果重复调用移除方法就会抛出异常NSRangeExxeption导致程序Crash;官方推荐的方式是在观察者初始化期间(init或viewDidLoad)注册,在dealloc时调用移除
防止多次注册和移除KVO的方式有三种:
-
利用@try @catch(只能针对删除多次KVO的情况),给NSObject增加一个分类,然后利用Runtime的
method_exchangeImplementations交换系统的removerObserver:方法在里面添加@try @catch -
利用模型数组进行存储记录
-
利用observationInfo里私有属性
-
如果对象被注册成为观察者,则该对象必须实现监听方法;当被观察者属性发生改变时就会调用监听方法,没有实现就会导致Crash
-
keyPath传入的是一个字符串,为避免写错可以使用NSStringFromSelector(@selector(propertyName)),将属性的getter方法SEL转换成字符串,这样编译阶段就会对keyPath进行检查
-
如果注册方法中context传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问ceontext就会Crash
-
如果监听集合对象改变,需要通过KVC的
mutableArrayValueForKey:等方法获取代理对象,并使用代理对象进行操作,当代理对象内部发生变化时会触发KVO,如果直接对集合对象进行操作改变不会触发KVO
KVO的底层实现: 利用Runtime API 动态生成一个继承自原有类的子类 NSKVONotifying_XXX, 实现原有类的setter方法 保留成员变量赋值是在原来类中进行,并且让instance对象的isa指向这个全新的子类,当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
1> 执行 willChangeValueForKey:
2> 父类原来的setter方法
3> 执行 didChangeValueForKey: ,内部会触发监听器(Observe)的监听方法--> observeValueForKeyPath:ofObject:change:context
最常见的使用方法
KVO 衍生类图解:
注:
NSKVONotifying_XXX :KVO监听时系统自动生成的类
NSKVONotifying_XXX类对象中存储了 :
isa指针 : 指向元类对象,而这个元类对象的ISA指向 NSObject 的根元类
superclass 指针:指向原来的实例对象的类对象,也就是说指向在添加KVO监听之前的类对象;而这个类对象在该实例对象添加KVO监听后成为了衍生类的父类。
setAge: 衍生类重写了属性setter方法,用来赋值;但实际赋值操作仍在原有类中实现
class : 衍生类重写class方法是为了不希望开发者了解到系统API的内部实现。直接返回原有类的类对象
dealloc: 衍生类重写dealloc方法便于管理监听的移除
_isKVOA: 此方法是为了给开发者一个可以判定对象是否添加KVO监听服务
疑问:为什么不重写getter方法?
因为getter中的实现是直接返回成员变量,而且通常是系统自动实现;子类不需要再重写,父类中有了就可以。
KVO属性监听的内部调用逻辑--伪代码实现如下
系统KVO缺点
- 使用比较麻烦,需要三个步骤:添加/注册KVO监听、实现监听方法、移除监听;需要手动移除观察者且移除时机必须合适,同时不能重复移除(会Crash)
- 在复杂业务逻辑中,准确判断被观察者相对比较麻烦,有多个被观察的属性或继承+多观察时需要写很多if判断
FBKVOController优点和使用
优点
- 会自动移除观察者对象
- 函数式编程,可以一行代码实现系统KVO的三个步骤
- 实现KVO与事件发生处的代码上下文相同,不需要跨方法传参数
- 增加了block和SEL自定义操作对NSKeyValueObserving回调的处理支持
- 每一个keyPath会对应一个block或者SEL,不需要使用if判断keyPath
- 可以同时对一个对象的多个属性进行监听,写法简洁(
NSHashTable<_FBKVOInfo *> *_infos) - 线程安全(内部使用了
pthread_mutex_lock)
使用
FBKVOController实现了观察者和被观察者的角色反转,系统的KVO是被观察者添加观察者,而FBKVO实现了观察者主动添加被观察者,实现角色上的反转,使用比较方便
系统KVO
- target: 被观察者
- observer: 观察者
[self.persion addObserver:self forKeyPath:NSStringFromSelector(@selector(myList)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
---------------------------------------------------------------
FBKVO
- target: 观察者
- observer: 被观察者
__weak typeof(self) waekSelf = self;
[self.KVOController observe:viewModel keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
//监听block回调
}];
FBKVOController原理