KVO
是一种机制,它允许对象在其他对象的指定属性发生更改时收到通知。它最常用的一个场景就是viewconroller
中监听model
属性的变化从而刷新页面展示。
KVO
使用过程的细节
基本使用
-
[self.person addObserver:self forKeyPath:@"nickname" options:NSKeyValueObservingOptionNew context:NULL];
-
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if ([keyPath isEqualToString:@"nickname"]) { NSLog(@"%@",change); } }
-
[self.person removeObserver:self forKeyPath:@"nickname" context:NULL];
context参数的使用
-
[self.person addObserver:self forKeyPath:@"nickname" options:NSKeyValueObservingOptionNew context:NULL]; - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if ([keyPath isEqualToString:@"nickname"]) { NSLog(@"%@",change); } }
-
//context定义 static void *PersonNickNameContext = &PersonNickNameContext; static void *PersonNameContext = &PersonNameContext; //注册观察者 [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickNameContext]; [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext]; //KVO的回调 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{ if (context == PersonNickNameContext) { NSLog(@"%@",change); }else if (context == PersonNameContext){ NSLog(@"%@",change); } }
是否有必要移除KVO
observer
官方文档对removeObserver
也有说明:
When removing an observer, keep several points in mind:
- Asking to be removed as an observer if not already registered as one results in an
NSRangeException
. You either callremoveObserver:forKeyPath:context:
exactly once for the corresponding call toaddObserver:forKeyPath:options:context:
, or if that is not feasible in your app, place theremoveObserver:forKeyPath:context:
call inside a try/catch block to process the potential exception.- An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
- The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in
init
orviewDidLoad
) and unregister during deallocation (usually indealloc
), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.
翻译过来就是,移除观察者时,注意以下几点:
- 如果未注册为观察者,在移除观察者的时候会导致
NSRangeException
异常。removeObserver
必须和addObserver
对应,且只能调用一次。如果项目中不能保证,就需要在使用的时候使用try/catch
来处理异常。
- 观察者在对象销毁的时候不会自动移除观察者。被观察者会继续发送通知,对观察者来说这个状态是感知不到的。但是,向一个已经释放的对象发送通知会引起内存访问异常。所以,我们要保证观察者在内存释放之前移除观察。
- 这个协议没有方法可以判断他是一个观察者还是被观察者,写代码是要避免释放内存相关的错误。一个典型的规范就是在观察者初始画的时候注册观察,在
dealloc
的时候移除观察,以确保成对和有序地添加和删除消息,并确保观察者在注册之前被取消注册,从内存中释放出来。 所以,总的来说,KVO注册观察者 和移除观察者是需要成对出现的
,如果只注册,不移除,会出现野指针的崩溃
。
自动触发与手动触发
KVO观察的自动和手动两种方式
-
自动开关,
automaticallyNotifiesObserversForKey
返回YES
的时候标示自动监听,如果是NO
表示我们需要手动监听// 自动开关 + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{ return YES; }
-
如果是手动,我们需要通过
手动开关
监听- (void)setNickName:(NSString *)nickName{ //手动开关 [self willChangeValueForKey:@"nickName"]; _nickName = namenickName [self didChangeValueForKey:@"nickName"]; }
观察多个属性变化
我们以观察两个属性为例,例如我们需要根据速度speed
和时间time
,取得当前的路程distance
。我们用两种方式。
- 第一种就是分别观察速度
speed
和时间time
两个属性,当其中一个发生变化计算 当前路程distance
。 - 第二种方式就是,通过
keyPathsForValuesAffectingValueForKey
方法,将两个观察合为一个观察,即观察当前路程distance
//1、合二为一的观察方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"distance"]) {
NSArray *affectingKeys = @[@"speed", @"time"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
//2、注册KVO观察
[self.person addObserver:self forKeyPath:@"distance" options:(NSKeyValueObservingOptionNew) context:NULL];
//3、触发属性值变化
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.person.speed += 10;
self.person.time += 1;
}
//4、移除观察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"distance"];
}
可变数组的观察
KVO是基于KVC基础之上的,所以可变数组如果直接添加数据,是不会调用setter方法的,所有对可变数组
的KVO观察下面这种方式不生效
的,即直接通过[self.person.dateArray addObject:@"1"];
向数组添加元素,是不会触发kvo通知回调的,代码如下:
//1、注册可变数组KVO观察者
self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
//2、KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
//3、移除观察者
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"dateArray"];
}
//4、触发数组添加数据
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person.dateArray addObject:@"1"];
}
The protocol defines three different proxy methods for collection object access, each with a key and a key path variant:
mutableArrayValueForKey:
andmutableArrayValueForKeyPath:
These return a proxy object that behaves like anNSMutableArray
object.mutableSetValueForKey:
andmutableSetValueForKeyPath:
These return a proxy object that behaves like anNSMutableSet
object.mutableOrderedSetValueForKey:
andmutableOrderedSetValueForKeyPath:
These return a proxy object that behaves like anNSMutableOrderedSet
object. 我们代码这样修改:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// KVC 集合 array
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
现在可变数组就可以监听到了。
KVO底层探索
苹果官方文档在Key-Value Observing Implementation Details
里有提到KVO
的实现:
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
的实现使用了isa
的交换。当我们添加一个observer
的时候isa
的指向会发生改变,是一个中间类
而不是真正的类
。我们不能根据isa
指针确定类的成员身份,而是用哪个class
方法确定。
中间类
是什么
看苹果官网文档我们了解了,KVO
的实现时通过修改isa
指针指向了一个中间类
实现的,我们使用lldb
探究一下中间类
是什么。
-
添加观察者之前,我们打印实例对象
person
的方法是JSPerson
。(lldb) po object_getClassName(self.person) "JSPerson"
-
添加观察者之后,我们打印实例对象
person
的方法是NSKVONotifying_JSPerson
。(lldb) po object_getClassName(self.person) "NSKVONotifying_JSPerson"
通过上面的调试,我们看到添加观察者值isa
指向了一个名为"NSKVONotifying_JSPerson
的中间类。关于这个中间类
我们有几个点需要研究一下。
中间类
和之前的类是父子类关系吗
我们通过一个方法来判断
#pragma mark - 遍历类以及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
//********以下为调用********
[self printClasses:[JSPerson class]];
打印结果为
classes = (
JSPerson,
"NSKVONotifying_JSPerson"
)
通过打印结果我们可以判断中间类NSKVONotifying_JSPerson
是JSPerson
的子类。
-
中间类里有什么方法。
同样,我们定义一个方法获取
NSKVONotifying_JSPerson
的所有方法#pragma mark - 遍历方法-ivar-property - (void)printClassAllMethod:(Class)cls{ unsigned int count = 0; Method *methodList = class_copyMethodList(cls, &count); for (int i = 0; i<count; i++) { Method method = methodList[i]; SEL sel = method_getName(method); IMP imp = class_getMethodImplementation(cls, sel); NSLog(@"%@-%p",NSStringFromSelector(sel),imp); } free(methodList); } //********以下为调用******** [self printClassAllMethod:objc_getClass("NSKVONotifying_JSPerson")];
打印结果
setNickName:-0x7fff207bbb57 class-0x7fff207ba662 dealloc-0x7fff207ba40b _isKVOA-0x7fff207ba403
我们看到一共有四个方法
- 重写了父类的
setNickName
方法 - 重写了根类的
class
、dealloc
方法 _isKVO
方法,用来判断是否是kvo
类
- 重写了父类的
-
dealloc中移除观察者后,isa会指回来吗
-
移除观察者之前,我们用
lldb
打印(lldb) po object_getClassName(self.person) "NSKVONotifying_JSPerson"
-
移除观察者之后,我们重新打印
(lldb) po object_getClassName(self.person) "JSPerson"
说明的确是在移除观察的时候将
isa
指回来的。 -
-
移除观察后中间类会销毁吗
我们返回前一个页面,此时添加观察者的VC已经销毁,我们打印
JSPerson
的子类[self printClasses:[JSPerson class]];
打印结果
classes = ( JSPerson, "NSKVONotifying_JSPerson" )
发现子类并不会被销毁。
小结
- 实例对象
isa
的指向在添加KVO观察者之后
,由原有类
更改为指向中间类
中间类
重写了观察属性的setter方法
、class
、dealloc
、_isKVOA
方法dealloc
方法中,移除KVO观察者之后,实例对象isa
指向由中间类
更改为原有类
中间类
从创建后,就一直存在内存中,不会被销毁