iOS---14---KVO
[toc]
什么是KVO
KVO
全称KeyValueObserving
,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO
的实现机制,所以对属性才会发生作用,一般继承自NSObject
的对象都默认支持KVO
。
KVO
和NSNotificationCenter
都是iOS
中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO
对被监听对象无侵入性,不需要修改其内部代码即可实现监听。
KVO
可以监听单个属性的变化,也可以监听集合对象的变化。通过KVC
的mutableArrayValueForKey:
等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO
监听的方法。集合对象包含NSArray
和NSSet
。
KVO使用
- 基础
- 通过
addObserver:forKeyPath:options:context:
方法注册观察者,观察者可以接收keyPath
属性的变化事件。 - 在观察者中实现
observeValueForKeyPath:ofObject:change:context
:方法,当keyPath
属性发生改变后,KVO
会回调这个方法来通知观察者。 - 当观察者不需要监听时,可以调用
removeObserver:forKeyPath:
方法将KVO
移除。需要注意的是,调用removeObserver
需要在观察者消失之前,否则会导致Crash
.
- 注册
-
在注册观察者时,可以传入
options
参数,参数是一个枚举类型。如果传入NSKeyValueObservingOptionNew
和NSKeyValueObservingOptionOld
表示接收新值和旧值,默认为只接收新值。如果想在注册观察者后,立即接收一次回调,则可以加入NSKeyValueObservingOptionInitial
枚举。 -
还可以通过方法
context
传入任意类型的对象,在接收消息回调的代码中可以接收到这个对象,是KVO
中的一种传值方式。 -
在调用
addObserver
方法后,KVO
并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash
- 监听方法
- 观察者需要实现
observeValueForKeyPath:ofObject:change:context:
方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash
。change
字典中存放KVO
属性相关的值,根据options
时传入的枚举来返回。枚举会对应相应key来从字典中取出值,例如有NSKeyValueChangeOldKey
字段,存储改变之前的旧值。 -
change
中还有NSKeyValueChangeKindKey
字段,和NSKeyValueChangeOldKey
是平级的关系,来提供本次更改的信息,对应NSKeyValueChange
枚举类型的value
。例如被观察属性发生改变时,字段为NSKeyValueChangeSetting
。 - 如果被观察对象是集合对象,在
NSKeyValueChangeKindKey字段中会包含
NSKeyValueChangeInsertion、
NSKeyValueChangeRemoval、
NSKeyValueChangeReplacement`的信息,表示集合对象的操作方式。
-
兼容方法
调用
KVO
属性对象时,不仅可以通过点语法和set
语法进行调用,KVO
兼容很多种调用方式。
//点语法
self.person.name = @"null";
//set方法
[self.person setName:@"test"];
// 数组变化
[self.person.dateArray addObject:@"1"];
//kpath
[self.person setValue:@"111" forKeyPath:@"person.name"];
// KVO 建立在 KVC
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
-
注意点
-
KVO
的addObserver和
removeObserver需要是成对的,如果重复
remove则会导致
NSRangeException类型的Crash,如果忘记
remove则会在观察者释放后再次接收到
KVO回调时
Crash`。
-
-
苹果官方推荐的方式是,在
init
的时候进行addObserver
,在dealloc
时removeObserver
,这样可以保证add
和remove
是成对出现的,是一种比较理想的使用方式。
KVO手动调用
可以看到调用KVO
主要依靠两个方法,在属性发生改变之前调用willChangeValueForKey
:方法,在发生改变之后调用didChangeValueForKey
:方法。
- (void)setNick:(NSString *)nick{
if([nick isEqualToString:_nick]){
return;
}
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
如果想控制当前对象的自动调用过程,也就是由上面两个方法发起的
KVO
调用,则可以重写下面方法。方法返回YES
则表示可以调用,如果返回NO
则表示不可以调用。
Return YES if the key-value observing machinery should automatically invoke -willChangeValueForKey:/-didChangeValueForKey:, -willChange:valuesAtIndexes:forKey:/-didChange:valuesAtIndexes:forKey:, or -willChangeValueForKey:withSetMutation:usingObjects:/-didChangeValueForKey:withSetMutation:usingObjects: whenever instances of the class receive key-value coding messages for the key, or mutating key-value coding-compliant methods for the key are invoked. Return NO otherwise. Starting in Mac OS 10.5, the default implementation of this method searches the receiving class for a method whose name matches the pattern +automaticallyNotifiesObserversOf[Key], and returns the result of invoking that method if it is found. So, any such method must return BOOL too. If no such method is found YES is returned.
返回 BOOL 类型
返回YES
的情况下,类的实例对象接收到KVC
类型的实例方法时,如setValueForKey:
等,或变相的遵守KVC
的该key
的访问器方法被调用时,如setKey:
等,KVO
内部机制会自动调用-willChangeValueForKey:
,-didChangeValueForKey:
等,自动发送 Change 通知。
-willChangeValueForKey:
, -didChangeValueForKey:
调用是触发通知的源头,这也就解释了手动发送Change
通知时,为何需要写这两个方法。
若不开启自动改变通知,则应返回NO
。
从Mac OS 10.5
开始,这个方法的默认实现逻辑是:会先查找消息接受方的类是否有 key 匹配的+automaticallyNotifiesObserversOf[Key]
方法,如:+automaticallyNotifiesObserversOfName
,若有,则返回匹配方法的结果,若没有则返回 YES
- 举例子
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
BOOL automatic = NO;
if ([key isEqualToString:@"nick"]) {
automatic = NO;
}else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.nick = @"Nil”;
}
打印发现没有触发,
如果set
方法这样写
- (void)setNick:(NSString *)nick{
if([nick isEqualToString:_nick]){
return;
}
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
打印有收到值改变
KVO实现原理
KVO
是通过isa-swizzling
技术实现的(这句话是整个KVO
实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa
指向中间类。并且将class
方法重写,返回原类的Class
。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
self.person = [[LGPerson alloc] init];
[self printClasses:[LGPerson class]];
[self printClassAllMethod:[LGPerson class]];
// [self printClassAllMethod:[LGStudent class]];
// [[LGStudent alloc] sayLove];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClasses:[LGPerson class]];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LGPerson")];
打印发现
2020-02-13 16:40:03.134467+0800 002---KVO原理探讨[2550:192081] classes = (
LGPerson,
LGStudent
)
2020-02-13 16:40:03.134705+0800 002---KVO原理探讨[2550:192081] *********************
2020-02-13 16:40:03.134907+0800 002---KVO原理探讨[2550:192081] sayHello-0x10b96c540
2020-02-13 16:40:03.135038+0800 002---KVO原理探讨[2550:192081] sayLove-0x10b96c550
2020-02-13 16:40:03.135146+0800 002---KVO原理探讨[2550:192081] .cxx_destruct-0x10b96c590
2020-02-13 16:40:03.135258+0800 002---KVO原理探讨[2550:192081] nickName-0x10b96c560
2020-02-13 16:40:03.135391+0800 002---KVO原理探讨[2550:192081] setNickName:-0x10b96c4e0
2020-02-13 16:40:53.117831+0800 002---KVO原理探讨[2550:192081] classes = (
LGPerson,
"NSKVONotifying_LGPerson",
LGStudent
)
2020-02-13 16:40:53.118036+0800 002---KVO原理探讨[2550:192081] *********************
2020-02-13 16:40:53.118152+0800 002---KVO原理探讨[2550:192081] setNickName:-0x10bcf0c7a
2020-02-13 16:40:53.118260+0800 002---KVO原理探讨[2550:192081] class-0x10bcef73d
2020-02-13 16:40:53.118420+0800 002---KVO原理探讨[2550:192081] dealloc-0x10bcef4a2
2020-02-13 16:40:53.118550+0800 002---KVO原理探讨[2550:192081] _isKVOA-0x10bcef49a
发现NSKVONotifying_LGPerson
新生成的类,已经不是之前的类了。KVO
会在运行时动态创建一个新类,将对象的isa指向新创建的类,新类是原类的子类,命名规则是NSKVONotifying_xx
的格式。KVO为了使其更像之前的类,还会将对象的class
实例方法重写,使其更像原类。
setNickName:-0x10bcf0c7a
KVO
会重写keyPath
对应属性的setter
方法,没有被KVO
的属性则不会重写其setter
方法。在重写的setter方法中,修改值之前会调用willChangeValueForKey:
方法,修改值之后会调用didChangeValueForKey:
方法,这两个方法最终都会被调用到observeValueForKeyPath:ofObject:change:context
:方法中。
-
调用
willChangeValueForKey
方法 -
调用
setAge
方法 -
调用
didChangeValueForKey
方法 -
didChangeValueForKey
方法内部调用oberser
的observeValueForKeyPath:ofObject:change:context
:方法 -
重写
- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
- 打印
2020-02-13 16:58:58.975829+0800 002---KVO原理探讨[2675:204649] willChangeValueForKey
2020-02-13 16:58:58.975941+0800 002---KVO原理探讨[2675:204649] didChangeValueForKey - begin
2020-02-13 16:58:58.976213+0800 002---KVO原理探讨[2675:204649] {
kind = 1;
new = KC;
}
2020-02-13 16:58:58.976374+0800 002---KVO原理探讨[2675:204649] didChangeValueForKey - end
KVO图示
![](https://img.haomeiwen.com/i2280900/67a6026b5c0ed682.jpg)
KVO面试题
-
通过修改类的成员变量不会触发KVO,那为什么通过KVC的 给成员变量赋值会触发KVO呢?
首先的基础是,我们看下的底层实现
1、首先搜索setKey
:方法.(key指成员变量名, 首字母大写)
2、上面的setter
方法没找到, 如果类方法返回YES. 那么按 _key, _isKey,key, iskey
的顺序搜索成员名。
如果找到了就会触发KVO
,因为底层内部会调用 和方法。
你可以重写该类的调用 和 方法去验证,当KVC改变属性值的时候,比如:Person
继承NSObject
,它有一个成员变量@public int _age;,[self.person1 setValue:@(10) forKey:@"age"];
会调用 和 方法。所以会触发。
通过修改类的成员变量不会触发KVO
,因为成员变量不会生成setter
方法,直接访问成员变量自然不会触发KVO,而要触发KVO
本质是必须调用调用 和 方法。KVO
的底层实现也是通过重写setter
方法 setter
方法里面调用和 方法。
-
如何手动触发KVO
1.手动调用willChangeValueForKey
和didChangeValueForKey
方法。 -
iOS
用什么方式实现对一个对象的KVO
?(KVO的本质是什么?)
- 利用
RuntimeAPI
动态生成一个子类NSKVONotifying_XXX
,并且让instance
对象的isa
指向这个全新的子类NSKVONotifying_XXX
- 当修改对象的属性时,会在子类
NSKVONotifying_XXX
调用Foundation
的_NSSetXXXValueAndNotify
函数 - 在
_NSSetXXXValueAndNotify
函数中依次调用 -1、willChangeValueForKey
-2、父类原来的setter
- 3、didChangeValueForKey,didChangeValueForKey
:内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context
:) - 如何手动触发
KVO
方法手动调用willChangeValueForKey
和didChangeValueForKey
方法键值观察通知依赖于NSObject
的两个方法:willChangeValueForKey
: 和didChangeValueForKey
。在一个被观察属性发生改变之前,willChangeValueForKey
: 一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey
会被调用,继而observeValueForKey:ofObject:change:context
: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了
有人可能会问只调用didChangeValueForKey
方法可以触发KVO
方法,其实是不能的,因为willChangeValueForKey
: 记录旧的值,如果不记录旧的值,那就没有改变一说了
-
直接修改成员变量会触发
KVO
吗
不会触发KVO
,因为KVO
的本质就是监听对象有没有调用被监听属性对应的setter方法,直接修改成员变量,是在内存中修改的,不走set方法 - 不移除
KVO
监听,会发生什么
- 不移除会造成内存泄漏
- 但是多次重复移除会崩溃。系统为了实现
KVO
,为NSObject
添加了一个名为NSKeyValueObserverRegistration
的Category
,KVO
的add
和remove
的实现都在里面。在移除的时候,系统会判断当前KVO
的key
是否已经被移除,如果已经被移除,则主动抛出一个NSException
的异常.