KVO的全称是Key-Value Observing, 俗称"键值监听", 可以用于监听某个对象属性值的改变
一、KVO的使用
- 新建工程, 定义
Person类继承自NSObject, 并添加int类型的属性age
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
@end
- 在
ViewController中添加两个Person类型的属性person1和person2, 并给person1添加监听age属性的观察者, 当点击屏幕时修改这两个对象的age属性值
@interface ViewController ()
@property (nonatomic, strong) Person *person1;
@property (nonatomic, strong) Person *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年龄"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1.age = 21;
self.person2.age = 22;
}
/**
当被观察的属性, 使用`set`方法赋值时, 触发观察者
@param keyPath 被监听的属性
@param object 添加监听的对象
@param change 属性改变前后的值
@param context 添加观察者时传入的参数
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"%@ - %@ - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc
{
[self.person1 removeObserver:self forKeyPath:@"age"];
}
@end
- 运行程序, 点击屏幕, 会有如下打印:
<Person: 0x60c000014b20> - age - {
kind = 1;
new = 21;
old = 1;
} - 年龄
-
打印中只有
person1的属性值发生改变信息, 而没有person2的属性值改变的信息 -
这是因为给
person1添加了观察者, 而person2没有添加 -
对象添加KVO监听属性, 类似于下图

问: 为什么同样都是调用了
setAge:方法,person1却能监听age的属性值改变?
二、添加KVO的对象的isa指针指向何处
- 下图就是上面创建的工程, 我们在
touchesBegan:withEvent:方法中 - 打断点, 当点击控制器的view后, 查看
person1和person2的isa指针指向何处

-
根据结果可以知道
person1的isa指向类对象NSKVONotifying_Personperson2的isa指向类对象Person
-
通过
person1的isa找到NSKVONotifying_Person后, 再次调用superclass, 可以看到NSKVONotifying_Person的父类是Person

- 说明: 添加了观察者(KVO)的对象, 它的
isa指针发生了改变, 指向了系统动态生成的子类NSKVONotifying_Person - 已经知道对象调用方法的过程:
- 首先通过
isa指针, 找到类对象 - 在类对象中查找方法, 如果方法存在就会调用
- 首先通过
- 所以
person1调用的setAge:方法, 是子类NSKVONotifying_Person中重写的setAge:方法 - 这就是为什么, 明明
person1和person2都调用了setAge:方法, 而person1会有属性监听
1、未使用KVO监听的Person对象

2、使用KVO监听的Person对象

- 上面是通过
isa验证了person1指向了NSKVONotifying_Person类, 下面使用代码进行验证 - 在给
person1添加观察者的前后, 分别打印person1和person2的类型
3、通过代码验证person1的类型是NSKVONotifying_Person
NSLog(@"%@ - %@",
object_getClass(self.person1),
object_getClass(self.person2));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年龄"];
NSLog(@"%@ - %@",
object_getClass(self.person1),
object_getClass(self.person2));
- 执行后打印如下:
Person - Person
NSKVONotifying_Person - Person
- 根据打印结果, 可以证明, 在给
person1添加观察者之后,person1的类型是NSKVONotifying_Person
三、验证NSKVONotifying_Person中setAge:的方法实现, 是_NSSetIntValueAndNotify函数
- 在
person1添加观察者的前后, 设置打印setAge方法地址的代码
NSLog(@"添加KVO之前 - %p - %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年龄"];
NSLog(@"添加KVO之后 - %p - %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
- 打印结果如下图:

-
很明显, 在添加KVO的前后,
person1调用的setAge:方法已经改变 -
下面使用lldb打印一下
setAge:方法

四、探索NSKVONotifying_Person类对象的isa, 指向何处
- 在添加KVO前后, 添加如下代码, 打印
person1的类对象和元类对象
NSLog(@"添加KVO之前 - %@ - %@",
object_getClass(self.person1),
object_getClass(object_getClass(self.person1)));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年龄"];
NSLog(@"添加KVO之前 - %@ - %@",
object_getClass(self.person1),
object_getClass(object_getClass(self.person1)));
- 打印结果如下

- 由打印可知:
NSKVONotifying_Person类对象的isa指向NSKVONotifying_Person的元类对象
五、通过逆向, 查看Fundation中的_NSSetIntValueAndNotify函数
- 使用
iFunBox查看越狱手机中的动态库文件

- 我使用的iPhone6, 所以这里查看arm64架构下的动态库文件
- 将
dyld_shared_cache_arm64文件托至电脑(复制)

- 通过终端, 使用
dsc_extractor对dyld_shared_cache_arm64进行分解
# 终端命令
./dsc_extractor dyld_shared_cache_arm64 test
- 分解出的
Fundation动态库如下

- 接下来使用反编译工具
hopper, 对Fundation反编译 - 使用
hopper打开Fundation动态库文件

- Nest

- OK, 可以看到反编译成功

- 搜索
_NSSetIntValueAndNotify, 可以看到Fundation中确实有_NSSetIntValueAndNotify函数

- 搜索
ValueAndNotify, 可以看到有很多类似的方法

-
根据搜索结果可以推断出, 对不同类型的属性添加观察, 就会调用对应属性类型的
_NSSet*ValueAndNotify方法,*表示类型 -
验证这个推断, 将
Person的age属性类型, 从int改为double
@interface Person : NSObject
@property (nonatomic, assign) double age;
@end
- 再次打印
setAge:方法实现:

- 根据结果, 验证推断正确
六、_NSSet*ValueAndNotify的内部实现
[self willChangeValueForKey:@"age"];
// 原来的setter实现
[self didChangeValueForKey:@"age"];
-
调用顺序:
- 调用
willChangeValueForKey: - 调用原来的
setter实现 - 调用
didChangeValueForKey:didChangeValueForKey:内部会调用observeValueForKeyPath:ofObject:change:context:
- 调用
-
通过代码验证, 在
Person.m中手动实现willChangeValueForKey和didChangeValueForKey以及setAge:方法, 代码如下:
@implementation Person
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"willChangeValueForKey - begin");
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
@end
- 运行程序, 点击控制器的View, 修改
person1的age属性, 有如下打印:
// 1
willChangeValueForKey - begin
// 2
willChangeValueForKey - end
// 3
setAge:
// 4
didChangeValueForKey - begin
// 5
<Person: 0x60800001a510> - age - {
kind = 1;
new = 21;
old = 1;
} - 年龄
// 6
didChangeValueForKey - end
- 根据打印: 可以证明以上的
_NSSet*ValueAndNotify的内部实现
七、子类内部的方法
-
在
使用KVO监听的Person对象的图片中,NSKVONotifying_Person的类对象中, 一共有两个指针isa和superclass, 四个方法setAge:,class,dealloc和_isKVOA -
下面使用
Runtime代码, 来验证NSKVONotifying_Person中确实存在这四个方法 -
首先在
ViewController中添加下面的方法:
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[array addObject:methodName];
}
NSLog(@"%@ - %@", cls, array);
}
- 同时删除
Person.m中的所有方法
@implementation Person
@end
- 接着在给
person1添加观察者的后面调用方法, 传入cls
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年龄"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
- 运行程序后, 有以下打印:
NSKVONotifying_Person - (
"setAge:",
class,
dealloc,
"_isKVOA"
)
Person - (
"setAge:",
age
)
- 可以看到NSKVONotifying_Person中有四个方法:
setAge:classdealloc_isKVOA
1、推断NSKVONotifying_Person中class方法的实现
- 首先在给
person1添加观察者的后面添加打印[self.person1 class]的代码
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"年龄"];
NSLog(@"%@", [self.person1 class]);
- 执行后, 打印结果如下:
// 打印结果:
Person
- 打印结果是
Person, 而person1的isa指向是NSKVONotifying_Person, 这说明在NSKVONotifying_Person中, 对class进行了重写 - 现在推断
NSKVONotifying_Person中的clss方法实现如下:
- (Class)class {
return [Preson class];
}
2、关于dealloc和_isKVOA方法
- 由于没办法看到
NSKVONotifying_Person中具体的源码, 所以只能模糊推断 - 因为
NSKVONotifying_Person是为了观察age属性, 才创建出来的, 所以在dealloc中会进行一些结尾操作 - 而
_isKVOA方法, 则推断为:
- (BOOL)_isKVOA {
return YES
}
八、面试题
1、iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么)
- 利用RuntimeAPI动态生成一个子类, 并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时, 会调用Fundation的_NSSet*ValueAndNotify函数
- willChangeValueForKey:
- 父类原来的setter方法
- didChangeValueForKey:
- 内部会触发监听器
Observer的监听方法(observeValueForKeyPath:ofObject:change:context:)
- 内部会触发监听器
2、如果直接修改对象的成员变量, 是否会触发监听器的(observeValueForKeyPath:ofObject:change:context:)方法?
- 将
Person类的_age暴露出来
@interface Person : NSObject
{
@public
int _age;
}
@property (nonatomic, assign) int age;
@end
- 将
ViewController中的touchesBegan:withEvent:方法修改如下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1->_age = 21;
}
- 运行程序后, 发现并没有触发
observeValueForKeyPath:ofObject:change:context:方法 - 所以, 直接修改对象的成员变量, 而不调用
set方法, 将不会触发观察者的observeValueForKeyPath:ofObject:change:context:方法
3、如何手动触发KVO?
- 已知实例对象被观察的属性, 在调用
set方法进行修改时, 会触发_NSSet*ValueAndNotify函数 - 并触发
willChangeValueForKey:和didChangeValueForKey:这两个方法, 所以我们可以手动添加这两个方法, 来触发KVO - 现在已知直接修改
成员变量时, 不会触发KVO, 那么就在修改成员变量的前后添加这两个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.person1 willChangeValueForKey:@"age"];
self.person1->_age = 21;
[self.person1 didChangeValueForKey:@"age"];
}
- 运行程序, 点击ViewController的view, 有如下打印:
<Person: 0x60000001c6c0> - age - {
kind = 1;
new = 21;
old = 1;
} - 年龄
- 所以, 通过调用
willChangeValueForKey:和didChangeValueForKey:方法, 就可以手动的调用KVO
注意:
willChangeValueForKey:和didChangeValueForKey:, 两个方法必须同时出现, 如果只有一个, 将不会触发KVO
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.person1->_age = 21;
[self.person1 didChangeValueForKey:@"age"];
}
- 运行程序, 点击屏幕后, 没有任何打印