本文首发于 个人博客
KVO
一直是IOS
面试中的重点,下面的面试题你碰到过吗?
-
KVO
的底层是如何实现的? -
addObserver:forKeyPath:options:context:
的context
有什么用? - 直接修改成员变量会触发
KVO
吗? - 我们知道
KVC
会修改成员变量,那么它会触发KVO
吗? - 如何监听可变数组的改变?
看到上述问题,你有答案了吗?如果你有疑惑,带着疑问我们开启一段KVO
的探索之旅。
KVO
全称Key-Value Observing
,是苹果提供的一套事件通知机制,允许一个对象在其他对象的指定属性发生更改时得到通知的机制。
KVO初探
大家都了解KVO
的基本使用方法,无非就是添加观察者、接收通知和移除观察者,下面我们通过一个简单的Demo
来了解一下具体的实现。
#import "ViewController.h"
#import "SSBoy.h"
#import "SSGirl.h"
#import <objc/runtime.h>
@interface ViewController ()
@property (nonatomic, strong) SSBoy *boy;
@property (nonatomic, strong) SSGirl *girl;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.boy = [SSBoy new];
self.girl = [SSGirl new];
// 添加观察者
[self.boy addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
[self.boy addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
[self.girl addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 修改相应的值
self.boy.name = @"sanliangsan";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
// 接收通知回调
if ([object isEqual:self.boy]) {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"boy change");
}
} else {
NSLog(@"girl change");
}
}
@end
上述代码清晰标注了KVO
的简单实现,不过这份简单代码有一些问题哦,那么问题在哪呢?
boy
和girl
同时观察了相同的name
属性,我们的observeValueForKeyPath
方法的接收中多层的嵌套判断比较复杂,而且还容易出错,这就引出了上述关于context
的面试题,我们引用一段官方文档:
You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons. A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
我们可以指定NULL
作为context
,但是这样会因为一些不同的原因导致对象的父类也同时会观察相同的属性key
,使用context
可以更安全以及更具有扩展性。同时也告知了我们context
如果为空应该是用NULL
而非nil
。
Context
具体如何使用呢?
// 定义context
static void *BoyNameContext = &BoyNameContext;
// 添加观察者
[self.boy addObserver:self forKeyPath:@"name"options:NSKeyValueObservingOptionNew context:BoyNameContext];
// 接收通知回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == BoyNameContext) {
// do....
}
}
Context
你会用了吗?聪明的你可能还发现了最上面的简单例子还有一个致命的问题!!!
正是:我们没有对相应观察者进行移除,在我们的观察者释放的时候我们要移除相应观察。
- (void)dealloc {
[self.boy removeObserver:self forKeyPath:@"name" context:BoyNameContext];
}
如果观察的对象是一个单例,而他在几个不同的场景都有观察同样的属性,那么在某个场景消失的时候别的地方触发属性修改就会导致单例去寻找已经释放的对象,就是野指针的情况。具体的实现还请各位自己去测试。到此KVO
的简单实现你会了吗?
接下来我们看看KVO
底层到底是如何实现的。
KVO底层原理
Automatic key-value observing is implemented using a technique called isa-swizzling. 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 the class method to determine the class of an object instance.
动态子类
KVO
底层的实现是运用了一项 isa-swizzling
的技术,当我们添加观察者的时候,系统动态的给我们的对象创建了一个子类,将对象的isa
指向了动态子类,而KVO
的所有实现都是通过这个动态子类的,添加一个动态子类让类的职责更单一具体,而且让我们的KVO
透明化,创建动态子类的过程我们是无法感知的,同时我们也知道了获取一个类不能通过isa
的指向而是要看class
的方法返回。多说无益,我们验证一下:
还是同样的代码,我们看到添加观察者之前,isa
指向SSBoy
,但是添加观察者之后就指向一个叫NSKVONotifying_SSBoy
的类,这正好验证了上述文档所说。
成员变量和属性
KVO
到底研究的是什么?
说到属性,我们无不和实例变量牵扯到一起,他们之间的区别就是是否有
setter
方法,属性的修改我们在最初已经验证过了,现在我们看看修改实例变量是否会触发KVO
。
// 实例变量
@interface SSBoy : NSObject
{
@public
NSString *nickName;
}
// 添加观察者
[self.boy addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:BoyNameContext];
// 修改实例变量
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 修改相应的值
self.boy->nickName = @"sanliangsan";
}
// 没有响应
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
// 接收通知回调
if (context == BoyNameContext) {
}
}
最终我们的结果是不会触发,那么为什么成员变量的修改不会触发KVO
,动态子类究竟都干了啥?
动态子类做了啥?
了解一个类我们无非是从属性,成员变量,方法等去研究,这里我们从方法入手,其他的大家可以下去一一验证。
我们去打印原类和动态子类的所有方法作以对比。
-
NSKVONotifying_SSBoy
中为啥没有setAge
?因为没有针对age添加观察者,这也证明了KVO的动态中间子类是通过实现setter方法去实现的。
-
NSKVONotifying_SSBoy
中为啥有class
方法?重写class方法,因为class指向类本身,【伪装】为了让这一层更透明,苹 果重写class方法重新指向SSBoy,让上层对动态子类的生成没有感知,透明化&隐私化
-
dealloc
方法为了啥?最初我们已经了解了,在添加观察者的时候会动态生成子类,而且对 象的isa会指向动态子类,当动态子类调用dealloc的时候,isa当然会重新指向回原类。
KVO与KVC
之前的一篇文章 KVC原理与自定义 有讲述KVC
底层是如何一步一步实现修改对象的属性的,那么问题来了,KVC
会触发KVO
吗,仔细阅读KVO
官方文档我们看到一段话:
NSObject provides a basic implementation of automatic ke y-value change notification. Automatic key-value change no tification informs observers of changes made using key-val ue compliant accessors, as well as the key-value coding me thods. Automatic notification is also supported by the col lection proxy objects returned by, for example, mutableAr rayValueForKey:
大概意思就是NSObject
提供了自动key-value
观察的实现,而且通过setter
方法和key-value coding
方法是一样的,换言之:key-value coding
也能实现自动KVO
,同时文档还给出相关能触发KVO
的实例:
Examples of method calls that cause KVO change notifications to be emitted.
// Call the accessor method.
[account setName:@"Savings"];
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
通过以上我们得知:KVC
是能自动实现KVO
的,而且可以验证不论是否有某属性都会自动通知到观察者。
KVO与可变数组
我们在某些情况下想对一个数组进行观察,添加、删除,修改等等,但是实际测试发现,普通的方法调用并不会触发KVO
,其原因很简单,利用我们上述的原理就得以解释:我们对数组的各种添加、删除、修改并不会调用setter
方法,由于KVC
会触发KVO
我们在KVC
里边找到相关的方法得以实现:
[[_arrayModel mutableArrayValueForKeyPath:@"dataArray"] addObject:XXX];
[[_arrayModel mutableArrayValueForKeyPath:@"dataArray"] removeObject:XXX];
至此,我们对KVO
的简单使用以及原理分析已经完结,那些面试题的答案你都知晓了吗?实际的使用过程中我们还会碰到更多的问题,期待你的交流和沟通。