目录
- 1. 什么是 KVO
- 2. KVO 的基本使用
2.1 注册方法
2.2 监听方法
2.3 移除方法
2.4 使用示例
2.5 实际应用
2.6 KVO 触发监听方法的方式
2.6.1 自动触发
2.6.2 手动触发- 3. KVO 的进阶使用
3.1 observationInfo 属性
3.2 context 的使用
3.3 KVO 监听集合对象
3.4 KVO 的自动触发控制
3.5 KVO 的手动触发
3.6 KVO 新旧值相等时不触发
3.7 KVO 手动观察集合属性
3.8 KVO 的依赖观察
3.8.1 一对一关系
3.8.2 一对多关系- 4. KVO 的使用注意
4.1 移除观察者的注意点
4.2 防止多次注册和移除相同的 KVO
4.3 其它注意点- 5. KVO 的实现原理
5.1 isa-swizzling
5.2 KVO 动态生成的子类都有哪些方法- 6. FBKVOController
6.1 系统 KVO 的缺点
6.2 FBKVOController 的介绍
6.3 FBKVOController 的优点
6.4 FBKVOController 的使用
6.5 FBKVOController 的解析- 参考
1. 什么是 KVO
-
KVO
的全称是Key-Value Observing
,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO
的监听方法来通知观察者。KVO
是在MVC
应用程序中的各层之间进行通信的一种特别有用的技术。 -
KVO
和NSNotification
都是iOS
中观察者模式的一种实现。 -
KVO
可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVC
的mutableArrayValueForKey:
等可变代理方法获得集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO
的监听方法。集合对象包含NSArray
和NSSet
。 -
KVO
和KVC
有着密切的关系,如果想要深入了解KVO
,建议先学习KVC
。
传送门:iOS - 关于 KVC 的一些总结
2. KVO 的基本使用
KVO
使用三部曲:添加/注册KVO
监听、实现监听方法以接收属性改变通知、 移除KVO
监听。
- 调用方法
addObserver:forKeyPath:options:context:
给被观察对象添加观察者; - 在观察者类中实现
observeValueForKeyPath:ofObject:change:context:
方法以接收属性改变的通知消息; - 当观察者不需要再监听时,调用
removeObserver:forKeyPath:
方法将观察者移除。需要注意的是,至少需要在观察者销毁之前,调用此方法,否则可能会导致Crash
。
2.1 注册方法
/*
** target: 被观察对象
** observer:观察者对象
** keyPath: 被观察对象的属性的关键路径,不能为nil
** options: 观察的配置选项,包括观察的内容(枚举类型):
NSKeyValueObservingOptionNew:观察新值
NSKeyValueObservingOptionOld:观察旧值
NSKeyValueObservingOptionInitial:观察初始值,如果想在注册观察者后,立即接收一次回调,可以加入该枚举值
NSKeyValueObservingOptionPrior:分别在值改变前后触发方法(即一次修改有两次触发)
** context: 可以传入任意数据(任意类型的对象或者C指针),在监听方法中可以接收到这个数据,是KVO中的一种传值方式
如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
2.2 监听方法
如果对象被注册成为观察者,则该对象必须能响应以下监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。如果没有实现就会导致Crash
。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/*
** keyPath:被观察对象的属性的关键路径
** object: 被观察对象
** change: 字典 NSDictionary<NSKeyValueChangeKey, id>,属性值更改的详细信息,根据注册方法中options参数传入的枚举来返回
key为 NSKeyValueChangeKey 枚举类型
{
1.NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key)
{
对应枚举类型 NSKeyValueChange
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
如果是对被观察对象属性(包括集合)进行赋值操作,kind 字段的值为 NSKeyValueChangeSetting
如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则会根据集合对象的操作方式来设置 kind 字段的值
插入:NSKeyValueChangeInsertion
删除:NSKeyValueChangeRemoval
替换:NSKeyValueChangeReplacement
}
2.NSKeyValueChangeNewKey:存储新值(如果options中传入NSKeyValueObservingOptionNew,change字典中就会包含这个key)
3.NSKeyValueChangeOldKey:存储旧值(如果options中传入NSKeyValueObservingOptionOld,change字典中就会包含这个key)
4.NSKeyValueChangeIndexesKey:如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则change字典中就会包含这个key
这个key的value是一个NSIndexSet对象,包含更改关系中的索引
5.NSKeyValueChangeNotificationIsPriorKey:如果options中传入NSKeyValueObservingOptionPrior,则在改变前通知的change字典中会包含这个key。
这个key对应的value是NSNumber包装的YES,我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES]
}
** context:注册方法中传入的context
*/
}
2.3 移除方法
在调用注册方法后,KVO
并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用以下方法移除观察者,否则如果在观察者被释放后,再次触发KVO
监听方法就会导致Crash
。
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
2.4 使用示例
以下使用KVO
为person
对象添加观察者为当前viewController
,监听person
对象的name
属性值的改变。当name
值改变时,触发KVO
的监听方法。
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person.name= @"张三";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"keyPath:%@",keyPath);
NSLog(@"object:%@",object);
NSLog(@"change:%@",change);
NSLog(@"context:%@",context);
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"name"];
}
keyPath:name
object:<HTPerson: 0x600003ae4340>
change:{ kind = 1; new = "\U70b9\U51fb"; old = "<null>"; }
context:(null)
2.5 实际应用
KVO
主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用KVO
实现最为合适。斯坦福大学的iOS
教程中有一个很经典的案例,通过KVO
在Model
和Controller
之间进行通信。如图所示:
2.6 KVO触发监听方法的方式
KVO
触发分为自动触发和手动触发两种方式。
2.6.1 自动触发
① 如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发KVO
:
- 使用点语法
- 使用
setter
方法 - 使用
KVC
的setValue:forKey:
方法 - 使用
KVC
的setValue:forKeyPath:
方法
② 如果是监听集合对象的改变,需要通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO
。集合对象包含NSArray
和NSSet
。
2.6.2 手动触发
① 普通对象属性或是成员变量使用:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
② NSArray
对象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
③ NSSet
对象使用:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
3. KVO 的进阶使用
3.1 observationInfo 属性
-
observationInfo
属性是NSKeyValueObserving.h
文件中系统通过分类给NSObject
添加的属性,所以所有继承于NSObject
的对象都含有该属性; - 可以通过
observationInfo
属性查看被观察对象的全部观察信息,包括observer
、keyPath
、options
、context
等。
@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
3.2 context 的使用
注册方法addObserver:forKeyPath:options:context:
中的context
可以传入任意数据,并且可以在监听方法中接收到这个数据。
context
作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值。
KVO
只有一个监听回调方法observeValueForKeyPath:ofObject:change:context:
,我们通常情况下可以在注册方法中指定context
为NULL
,并在监听方法中通过object
和keyPath
来判断触发KVO
的来源。
但是如果存在继承的情况,比如现在有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对 account 对象的 balance 属性进行观察。问题:
① 当 balance 发生改变时,应该由谁来处理呢?
② 如果都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是自己的事务还是子类对象的事务呢?
这时候通过使用context
就可以很好地解决这个问题,在注册方法中为context
设置一个独一无二的值,然后在监听方法中对context
值进行检验即可。苹果的推荐用法:用
context
来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为context
的值。可以为整个类设置一个context
,然后在监听方法中通过object
和keyPath
来确定被观察属性,这样存在继承的情况就可以通过context
来判断;也可以为每个被观察对象属性设置不同的context
,这样使用context
就可以精确的确定被观察对象属性。
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[account addObserver:self
forKeyPath:@"interestRate"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountInterestRateContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
// Any unrecognized context must belong to super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
-
context
优点:嵌套少、性能高、更安全、扩展性强。 -
context
注意点:
① 如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context
就可能导致Crash
;
② 空传NULL
而不应该传nil
。
3.3 KVO 监听集合对象
KVO
可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO
的监听方法。集合对象包含NSArray
和NSSet
。
(注意:如果直接对集合对象进行操作改变,不会触发KVO
。)
示例代码及输出如下:
观察者 viewController 对被观察对象 person 的 mArray 属性进行监听。
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [HTPerson new];
self.person.mArray = [NSMutableArray arrayWithCapacity:5];
[self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew| NSKeyValueObservingOptionOld) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self.person.mArray addObject:@"2"]; //如果直接对数组进行操作,不会触发KVO
NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
[array addObject:@"1"];
[array replaceObjectAtIndex:0 withObject:@"2"];
[array removeObjectAtIndex:0];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
/* change 字典的值为:
{
indexes:对应的值为数组操作的详细信息,包括索引等
kind: 对应的值为数组操作的方式:
2:代表插入操作
3:代表删除操作
4:代表替换操作
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
new/old:如果是插入操作,则字典中只会有new字段,对应的值为插入的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew)
如果是删除操作,则字典中只会有old字段,对应的值为删除的元素,前提条件是options中传入了(NSKeyValueObservingOptionOld)
如果是替换操作,则字典中new和old字段都可以存在,对应的值为替换后的元素和替换前的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)
如: indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
1
);
}
*/
NSLog(@"%@",change);
}
- (void)dealloc
{
[self.person removeObserver:self forKeyPath:@"mArray"];
}
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 2; new = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 4; new = (2); old = (1); }
{ indexes = "<_NSCachedIndexSet: 0x6000030e5380>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; kind = 3; old = (2); }
3.4 KVO 的自动触发控制
可以在被观察对象的类中重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
方法来控制KVO
的自动触发。
如果我们只允许外界观察 person 的 name 属性,可以在 Person 类如下操作。这样外界就只能观察 name 属性,即使外界注册了对 person 对象其它属性的监听,那么在属性发生改变时也不会触发KVO
。
// 返回值代表允不允许触发 KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"name"]) {
automatic = YES;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
也可以实现遵循命名规则为+ (BOOL)automaticallyNotifiesObserversOf<Key>
的方法来单一控制属性的KVO
自动触发,<Key>
为属性名(首字母大写)。
+ (BOOL)automaticallyNotifiesObserversOfName
{
return NO;
}
注意:
- 第一个方法的优先级高于第二个方法。如果实现了
automaticallyNotifiesObserversForKey:
方法,并对<Key>
做了处理,则系统就不会再调用该<Key>
的automaticallyNotifiesObserversOf<Key>
方法。options
指定的NSKeyValueObservingOptionInitial
触发的KVO
通知,是无法被automaticallyNotifiesObserversForKey:
阻止的。
3.5 KVO 的手动触发
使用场景:
- 使用
KVO
监听成员变量值的改变; - 在某些需要控制监听过程的场景下。比如:为了尽量减少不必要的触发通知操作,或者当多个更改同时具备的时候才调用属性改变的监听方法。
由于KVO
的本质,重写setter
方法来达到可以通知所有观察者对象的目的,所以只有通过setter
方法或KVC
方法去修改属性变量值的时候,才会触发KVO
,直接修改成员变量不会触发KVO
。
当我们要使用KVO
监听成员变量值改变的时候,可以通过在为成员变量赋值的前后手动调用willChangeValueForKey:
和didChangeValueForKey:
两个方法来手动触发KVO
,如:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person willChangeValueForKey:@"age"];
self.person->_age = 18;
[self.person didChangeValueForKey:@"age"];
}
NSKeyValueObservingOptionPrior
(分别在值改变前后触发方法,即一次修改有两次触发)的两次触发分别在willChangeValueForKey:
和didChangeValueForKey:
的时候进行的。
如果注册方法中options
传入NSKeyValueObservingOptionPrior
,那么可以通过只调用willChangeValueForKey:
来触发改变前的那次KVO
,可以用于在属性值即将更改前做一些操作。
3.6 KVO 新旧值相等时不触发
有时候我们可能会有这样的需求,KVO
监听的属性值修改前后相等的时候,不触发KVO
的监听方法,可以结合KVO
的自动触发控制和手动触发来实现。
例如:对 person 对象的 name 属性注册了KVO
监听,我们希望在对 name 属性赋值时做一个判断,如果新值和旧值相等,则不触发KVO
,可以在 Person 类中如下这样实现,将 name 属性值改变的KVO
触发方式由自动触发改为手动触发。
+ (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:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
}
3.7 KVO 手动观察集合属性
有些情况下我们想手动观察集合属性,下面以观察数组为例。
关键方法:
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
需要注意的是,根据KVC
的NSMutableArray 搜索模式
:
传送门:iOS - 关于 KVC 的一些总结
- 至少要实现一个插入和一个删除方法,否则不会触发
KVO
。如
插入方法:insertObject:in<Key>AtIndex:
或insert<Key>:atIndexes:
删除方法:removeObjectFrom<Key>AtIndex:
或remove<Key>AtIndexes:
- 可以不实现替换方法,但是如果不实现替换方法,执行替换操作时,
KVO
会把它当成先删除后添加,即会触发两次KVO
。第一次触发的KVO
中change
字典的old
键的值为替换前的元素,第二次触发的KVO
中change
字典的new
键的值为替换后的元素,前提条件是注册方法中的options
传入对应的枚举值。 - 如果实现替换方法,则执行替换操作只会触发一次
KVO
,并且change
字典会同时包含new
和old
,前提条件是注册方法中的options
传入对应的枚举值。
替换方法:replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
- 建议实现替换方法以提高性能。
示例代码如下:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"mArray"]) {
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)insertMArray:(NSArray *)array atIndexes:(NSIndexSet *)indexes
{
[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray insertObjects:array atIndexes:indexes];
[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"mArray"];
}
- (void)removeMArrayAtIndexes:(NSIndexSet *)indexes
{
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray removeObjectsAtIndexes:indexes];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"mArray"];
}
- (void)replaceMArrayAtIndexes:(NSIndexSet *)indexes withMArray:(NSArray *)array
{
[self willChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
[self.mArray replaceObjectsAtIndexes:indexes withObjects:array];
[self didChange:NSKeyValueChangeReplacement valuesAtIndexes:indexes forKey:@"mArray"];
}
3.8 KVO 的依赖观察
3.8.1 一对一关系
有些情况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。
比如我们想要对 Download 类中的 downloadProgress 属性进行KVO
监听,该属性的改变依赖于 writtenData 和 totalData 属性的改变。观察者监听了 downloadProgress ,当 writtenData 和 totalData 属性值改变时,观察者也应该被通知。以下有两种方法可以解决这个问题。
- 重写以下方法来指明 downloadProgress 属性依赖于 writtenData 和 totalData:
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"writtenData",@"totalData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
- 实现一个遵循命名规则为
keyPathsForValuesAffecting<Key>
的类方法,<Key>
是依赖于其他值的属性名(首字母大写):
+ (NSSet<NSString *> *)keyPathsForValuesAffectingDownloadProgress
{
return [NSSet setWithObjects:@"writtenData",@"totalData", nil];
}
注意:以上两个方法可以同时存在,且都会调用,但是最终结果会以
keyPathsForValuesAffectingValueForKey:
为准。
3.8.2 一对多关系
以上方法在观察集合属性时就不管用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性。你希望 Department 类有一个 totalSalary 属性来计算所有员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于所有 Employee 实例对象的 salary 属性。以下有两种方法可以解决这个问题。
- 你可以用
KVO
将 parent(比如 Department )作为所有 children(比如 Employee )相关属性的观察者。你必须在把 child 添加或删除到 parent 时把 parent 作为 child 的观察者添加或删除。在observeValueForKeyPath:ofObject:change:context:
方法中我们可以针对被依赖项的变更来更新依赖项的值:
#import "Department.h"
static void *totalSalaryContext = &totalSalaryContext;
@interface Department ()
@property (nonatomic,strong)NSArray<Employee *> *employees;
@property (nonatomic,strong)NSNumber *totalSalary;
@end
@implementation Department
- (instancetype)initWithEmployees:(NSArray *)employees
{
self = [super init];
if (self) {
self.employees = [employees copy];
for (Employee *em in self.employees) {
[em addObserver:self forKeyPath:@"salary" options:NSKeyValueObservingOptionNew context:totalSalaryContext];
}
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == totalSalaryContext) {
[self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)setTotalSalary:(NSNumber *)totalSalary
{
if (_totalSalary != totalSalary) {
[self willChangeValueForKey:@"totalSalary"];
_totalSalary = totalSalary;
[self didChangeValueForKey:@"totalSalary"];
}
}
- (void)dealloc
{
for (Employee *em in self.employees) {
[em removeObserver:self forKeyPath:@"salary" context:totalSalaryContext];
}
}
@end
- 使用
iOS
中观察者模式的另一种实现方式:通知 (NSNotification
) 。
4. KVO的使用注意
4.1 移除观察者的注意点
- 在调用
KVO
注册方法后,KVO
并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用KVO
移除方法移除观察者,否则如果在观察者被释放后,再次触发KVO
监听方法就会导致Crash
。 -
KVO
的注册方法和移除方法应该是成对的,如果重复调用移除方法,就会抛出异常NSRangeException
并导致程序Crash
。 - 苹果官方推荐的方式是,在观察者初始化期间(
init
或者viewDidLoad
的时候)注册为观察者,在释放过程中(dealloc
时)调用移除方法,这样可以保证它们是成对出现的,是一种比较理想的使用方式。
4.2 防止多次注册和移除相同的KVO
有时候我们难以避免多次注册和移除相同的KVO
,或者移除了一个未注册的观察者,从而产生可能会导致Crash
的风险。
三种解决方案:黑科技防止多次添加删除KVO出现的问题
- 利用
@try @catch
(只能针对删除多次KVO
的情况下)
给NSObject
增加一个分类,然后利用Runtime API
交换系统的removeObserver
方法,在里面添加@try @catch
; - 利用 模型数组 进行存储记录;
- 利用
observationInfo
里私有属性。
4.3 其它注意点
- 如果对象被注册成为观察者,则该对象必须能响应监听方法,即该对象所属类中必须实现监听方法。当被观察对象属性发生改变时就会调用监听方法。如果没有实现就会导致
Crash
。所以KVO
三部曲缺一不可。 -
keyPath
传入的是一个字符串,为避免写错,可以使用NSStringFromSelector(@selector(propertyName))
,将属性的getter
方法SEL
转换成字符串,在编译阶段对keyPath
进行检验。 - 如果注册方法中
context
传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context
就可能导致Crash
。- 可以使用
__bridge_retained
桥接剥夺对象的内存管理权,但必须记得在不需要该对象时释放它,否则内存泄露。关于桥接可以参阅《iOS - 老生常谈内存管理(三):ARC 面世 —— Toll-Free Bridging》; - 或者使用全局变量。
- 可以使用
- 如果是监听集合对象的改变,需要通过
KVC
的mutableArrayValueForKey:
等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO
。如果直接对集合对象进行操作改变,不会触发KVO
。 - 在观察者类的监听方法中,应该为无法识别的
context
或者object
、keyPath
调用父类的实现[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
。
5. KVO的实现原理
Key-Value Observing Implementation Details
- Automatic key-value observing is implemented using a technique called isa-swizzling.
- The
isa
pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.- When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
- You should never rely on the
isa
pointer to determine class membership. Instead, you should use theclass
method to determine the class of an object instance.
以上是苹果官方对KVO
实现的解释,只说明了KVO
是使用isa-swizzling
技术来实现的,并没有做过多介绍。
5.1 isa-swizzling
苹果使用了isa
混写技术(isa-swizzling
)来实现KVO
。当我们调用了addObserver:forKeyPath:options:context:
方法,为instance
被观察对象添加KVO
监听后,系统会在运行时利用Runtime API
动态创建instance
对象所属类A
的子类NSKVONotifying_A
,并且让instance
对象的isa
指向这个全新的子类,并重写原类A
的被观察属性的setter
方法来达到可以通知所有观察者对象的目的。
这个子类的isa
指针指向它自己的meta-class
对象,而不是原类的meta-class
对象。
重写的setter
方法的SEL
对应的IMP
为Foundation
中的_NSSetXXXValueAndNotify
函数(XXX
为Key
的数据类型),当被观察对象的属性发送改变时,会调用_NSSetXXXValueAndNotify
函数,这个函数中会调用:
-
willChangeValueForKey:
方法 - 父类原来的
setter
方法 -
didChangeValueForKey:
方法(内部会触发监听器即观察对象observer
的监听方法:observeValueForKeyPath:ofObject:change:context:
)
在移除KVO
监听后,被观察对象的isa
会指回原类A
,但是NSKVONotifying_A
类并没有销毁,还保存在内存中。
5.2 KVO 动态生成的子类都有哪些方法
NSKVONotifying_A
除了重写了setter
方法,还重写了class
、dealloc
、_isKVOA
这三个方法(可以使用runtime
的class_copyMethodList
函数打印方法列表获得),其中:
-
class
:class
方法中返回的是父类的class
对象,目的是为了不让外界知道KVO
动态生成类的存在; -
dealloc
:释放KVO
使用过程中产生的东西; -
_isKVOA
:用来标志它是一个KVO
的类。
6. FBKVOController
6.1 系统 KVO 的缺点
- 使用比较麻烦,需要三个步骤:添加/注册
KVO
监听、实现监听方法以接收属性改变通知、 移除KVO
监听,缺一不可; - 需要手动移除观察者,移除观察者的时机必须合适,还不能重复移除;
- 注册观察者的代码和事件发生处的代码上下文不同,传递上下文
context
是通过void *
指针; - 需要实现
-observeValueForKeyPath:ofObject:change:context:
方法,比较麻烦; - 在复杂的业务逻辑中,准确判断被观察者相对比较麻烦,有多个被观测的对象和属性时,需要在方法中写大量的
if
进行判断。
6.2 FBKVOController 的介绍
FBKVOController
是 Facebook 开源的一个基于系统KVO
实现的框架。支持Objective-C
和Swift
语言。
GitHub:https://github.com/facebook/KVOController
6.3 FBKVOController 的优点
- 会自动移除观察者;
- 函数式编程,可以一行代码实现系统
KVO
的三个步骤; - 实现
KVO
与事件发生处的代码上下文相同,不需要跨方法传参数; - 增加了
block
和SEL
自定义操作对NSKeyValueObserving
回调的处理支持; - 每一个
keyPath
会对应一个block
或者SEL
,不需要使用if
判断keyPath
; - 可以同时对一个对象的多个属性进行监听,写法简洁;
- 线程安全。
6.4 FBKVOController 的使用
FBKVOController
实现了观察者和被观察者的角色反转,系统的KVO
是被观察者添加观察者,而FBKVO
实现了观察者主动去添加被观察者,实现了角色上的反转,使用比较方便。
// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
// observe clock date property
// 使用 block
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
// update clock view with new value
clockView.date = change[NSKeyValueChangeNewKey];
}];
// 使用 SEL
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew action:@selector(updateClockWithDateChange:)];
6.5 FBKVOController 的解析
如何优雅地使用KVO(简书)
iOS - FBKVOController 实现原理(简书)
参考
Key-Value Observing Programming Guide(苹果官方文档)
iOS - 关于 KVC 的一些总结(简书)
KVO原理分析及使用进阶(简书)
iOS开发 - 黑科技防止多次添加删除KVO出现的问题(简书)
谈谈 KVO(简书)
GitHub/facebook/KVOController(GitHub)
如何优雅地使用KVO(简书)
iOS - FBKVOController 实现原理(简书)