看一个例子
#import "ViewController.h"
#import <objc/runtime.h>
// ----------------------- Cat -----------------------
@interface Cat : NSObject
@property (nonatomic, assign) NSInteger age;
@end
@implementation Cat
@end
// ----------------------- ViewController -----------------------
@interface ViewController ()
@property (nonatomic, strong) Cat *whiteCat; // 对象
@property (nonatomic, strong) Cat *blackCat; // 对象
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.whiteCat = [[Cat alloc] init];
self.whiteCat.age = 2;
self.blackCat = [[Cat alloc] init];
self.blackCat.age = 3;
// 为 whiteCat 对象添加 KVO,blackCat 对象保持不变
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.whiteCat addObserver:self forKeyPath:@"age" options:options context:@"Kitten"];
}
// 监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == @"Kitten") {
NSLog(@"KVO监听 %@, %@, %@", object, keyPath, change);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// 测试代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.whiteCat.age = 5;
}
// 记得移除监听
- (void)dealloc {
[self.whiteCat removeObserver:self forKeyPath:@"age"];
}
@end
代码干的事情很简单:对 whiteCat 对象的 age 属性做 KVO 监听。如果此时点击屏幕可以看的输出:
2021-01-01 20:08:35.562295+0800 KVO[69525:1562303] KVO监听 <Cat: 0x600002f9c310>, age, {
kind = 1;
new = 5;
old = 2;
}
可以看出触发了属性 age 的 KVO
看看添加KVO前后的变化
在 viewDidLoad 方法里面添加输出代码, 查看 whiteCat对象 和 blackCat对象它们指向的类。
- (void)viewDidLoad {
[super viewDidLoad];
self.whiteCat = [[Cat alloc] init];
self.whiteCat.age = 2;
self.blackCat = [[Cat alloc] init];
self.blackCat.age = 3;
NSLog(@"添加KVO前: whiteCat = %@, pointer = %p, blackCat = %@ ,pointer = %p", object_getClass(self.whiteCat), object_getClass(self.whiteCat), object_getClass(self.blackCat), object_getClass(self.blackCat));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.whiteCat addObserver:self forKeyPath:@"walkSteps" options:options context:@"cat walk"];
NSLog(@"添加KVO后: whiteCat = %@, pointer = %p, blackCat = %@, pointer = %p", object_getClass(self.whiteCat), object_getClass(self.whiteCat), object_getClass(self.blackCat), object_getClass(self.blackCat));
}
输出:
2021-01-01 20:16:09.187180+0800 KVO[69962:1571669] 添加KVO前: whiteCat = Cat, pointer = 0x1033b5690, blackCat = Cat ,pointer = 0x1033b5690
2021-01-01 20:16:09.187564+0800 KVO[69962:1571669] 添加KVO后: whiteCat = NSKVONotifying_Cat, pointer = 0x6000036881b0, blackCat = Cat, pointer = 0x1033b5690
奇怪的事情发生了:
我们知道在APP中一个类和它的元类只有一个,而通过类创建的对象可以有很多个,毕竟每个对象里面存储的实例变量的数据都是可以不一样的。
但是通过刚刚输出,可以看到:开始的时候whiteCat对象 和 blackCat对象它们指向的类都是 Cat 类,地址是0x1033b5690,但是为whiteCat对象添加KVO, blackCat对象不添加。这时whiteCat对象指向的类变成了 NSKVONotifying_Cat 地址为0x6000036881b0。好吧它指向的类改变了,也就是whiteCat对象的 isa指针从 Cat类 变成了 NSKVONotifying_Cat类。
看一下 NSKVONotifying_Cat 类的内存信息
打印一下 NSKVONotifying_Cat 类的前两个变量 isa 指针和 superclass 指针的内存信息:
(lldb) x/2xg 0x6000036881b0
0x6000036881b0: 0x0000600003688240 0x00000001033b5690
可以看的后面的 superclass 指针地址是 0x00000001033b5690。这个和前面输出的 Cat类 的内存地址是一样的,也就是说 NSKVONotifying_Cat 是 Cat 的子类。
打印一下 Cat 类的前两个变量 isa 指针和 superclass 指针的内存信息:
(lldb) x/2xg 0x1033b5690
0x1033b5690: 0x00000001033b5668 0x00007fff86d50660
可以看到 Cat 的 isa 指针地址指向和 NSKVONotifying_Cat 的 isa 指针地址指向不一样,也就是说它们的元类不一样。
画一下 whiteCat 添加 KVO 的变化
没有添加 KVO 的时候是这样的
虚线是对应的 isa 指针的指向,实线是对应的 superclass 指针的指向。
blackCat, whiteCat 都指向同一个类。
添加 KVO 以后是这样的
其中 NSKVONotifying_Cat 是类, MetaNSKVONotifying_Cat 是对应的元类。
Cat 是类, MetaCat 是对应的元类。
NSObject 是类, MetaNSObject 是对应的元类。
blackCat, whiteCat 是实例对象。
继承关系是这样的:NSKVONotifying_Cat: Cat: NSObject
所以添加 KVO 后发生了什么?
首先输出一下 Cat 和 NSKVONotifying_Cat 类的方法列表。添加输出方法:
// 主要是使用runtime输出看看
- (void)printMethodsNameForClass:(Class)cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodsName = [NSMutableString string];
for (int i = 0; i < count; i ++) {
Method item = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(item));
[methodsName appendFormat:@"\n%@", methodName];
}
free(methodList);
NSLog(@"class = %@, methods = %@", cls, methodsName);
}
方法里面主要是使用 runtime 获取类里面的方法列表。测试一下:
[self printMethodsNameForClass:object_getClass(self.blackCat)]; // blackCat 没有添加KVO
[self printMethodsNameForClass:object_getClass(self.whiteCat)]; // whiteCat 添加了KVO
输出:
2021-01-01 21:01:41.115662+0800 KVO[71854:1613416] class = Cat, methods =
setAge:
age
2021-01-01 21:01:41.115761+0800 KVO[71854:1613416] class = NSKVONotifying_Cat, methods =
setAge:
class
dealloc
_isKVOA
Cat 类里面有 setAge: 和 age 方法,这很好理解,毕竟 Cat 类里面有一个 age 属性。
NSKVONotifying_Cat 里面重写了父类的 setAge: 方法,然后额外重写了 class, dealloc, _isKVOA 方法。
根据实际使用KVO的情况和一些资料可以得到的 NSKVONotifying_Cat 大致的伪代码(具体的代码实现肯定有差异,但是实现的功能大致类似):
@implementation NSKVONotifying_Cat
- (void)setAge:(NSInteger)age {
[self willChangeValueForKey: @"age"];
// 调用Cat类实现具体的值修改
[super setAge: age];
// didChangeValueForKey 方法里面调用 [observer observeValueForKeyPath: ofObject: change: context:]; 完成KVO通知
[self didChangeValueForKey: @"age"];
}
- (Class)class {
// 重写返回 Cat 类,让用户感觉不到变化
return objc_getClass("Cat");
}
- (void)dealloc {
// 调整 whiteCat 的 isa 指针重新指向 Cat 类
}
- (BOOL)_isKVOA {
return true;
}
@end
验证一下
验证 setAge:方法
由于不能直接在 NSKVONotifying_Cat 类里面添加实现,那就直接在 Cat 类里面模拟了,添加如下测试代码:
@implementation Cat
- (void)setAge:(NSInteger)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
编译运行,点击测试:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.whiteCat.age = 5;
}
可以看到输出如下:
2021-01-01 21:28:10.945604+0800 KVO[73019:1645633] -----willChangeValueForKey: begin
2021-01-01 21:28:10.945798+0800 KVO[73019:1645633] -----willChangeValueForKey: end
2021-01-01 21:28:10.945904+0800 KVO[73019:1645633] -----setAge:
2021-01-01 21:28:10.946013+0800 KVO[73019:1645633] -----didChangeValueForKey: begin
2021-01-01 21:28:10.946280+0800 KVO[73019:1645633] KVO监听 <Cat: 0x600001a78660>, age, {
kind = 1;
new = 5;
old = 2;
}
2021-01-01 21:28:10.946407+0800 KVO[73019:1645633] -----didChangeValueForKey: end
的确调用 self.whiteCat.age = 5,执行 NSKVONotifying_Cat 的 setAge: 方法会执行类似上面的代码。
验证 class 方法
添加测试 class 方法 代码:
- (void)viewDidLoad {
[super viewDidLoad];
self.whiteCat = [[Cat alloc] init];
self.whiteCat.age = 2;
self.blackCat = [[Cat alloc] init];
self.blackCat.age = 3;
// 查看变化
NSLog(@"添加KVO前: whiteCat = %@, pointer = %p, blackCat = %@ ,pointer = %p", [self.whiteCat class], [self.whiteCat class], [self.blackCat class], [self.blackCat class]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.whiteCat addObserver:self forKeyPath:@"age" options:options context:@"Kitten"];
// 查看变化
NSLog(@"添加KVO后: whiteCat = %@, pointer = %p, blackCat = %@ ,pointer = %p", [self.whiteCat class], [self.whiteCat class], [self.blackCat class], [self.blackCat class]);
}
输出:
2021-01-01 21:32:08.492332+0800 KVO[73170:1649171] 添加KVO前: whiteCat = Cat, pointer = 0x10e5146a8, blackCat = Cat ,pointer = 0x10e5146a8
2021-01-01 21:32:08.492607+0800 KVO[73170:1649171] 添加KVO后: whiteCat = Cat, pointer = 0x10e5146a8, blackCat = Cat ,pointer = 0x10e5146a8
可以看到调用 class 方法返回的都是 Cat 类,它们的内存指向都一样。
为什么要对 class 方法做这样的操作? 这样的一个好处就是如果用户不是直接调用 runtime的方法,那么是感觉不到添加KVO之后的变化的。毕竟只需要知道自己添加了KVO实现了功能就好了。
问答
1. KVO 的本质是什么?
通过上面的一系列步骤我们知道当一个对象的属性添加了KVO之后,runtime 会动态创建一个原来类的一个子类,然后修改对象的 isa 指针指向,使得从原来的类指向它的子类。
具体的实现是在动态创建的子类里面重写父类的属性的 set: 方法,和 class, dealloc, _isKVOA 方法。在 set 方法里面大致是做了这样一个事情:
- (void)setXXX:(int)xxx
{
[self willChangeValueForKey:@"xxx"];
[super setXXX:xxx];
[self didChangeValueForKey:@"xxx"];
}
2. 如何手动触发 KVO?
我们知道触发 KVO 的实际干了的事情是在属性的 set 方法里面大致做了如下工作:
- (void)setXXX:(int)xxx
{
[self willChangeValueForKey:@"xxx"];
[super setXXX:xxx];
[self didChangeValueForKey:@"xxx"];
}
那么想要手动触发 KVO,自己调用下面的代码就好了 :
[self willChangeValueForKey:@"xxx"];
[self didChangeValueForKey:@"xxx"];
验证一下 (下面代码不调用属性的set方法了):
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == @"Kitten") {
NSLog(@"KVO监听 %@, %@, %@", object, keyPath, change);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 手动触发
[self.whiteCat willChangeValueForKey:@"age"];
[self.whiteCat didChangeValueForKey:@"age"];
}
输出:
2021-01-01 21:47:36.566013+0800 KVO[73701:1660860] KVO监听 <Cat: 0x6000020a0200>, age, {
kind = 1;
new = 2;
old = 2;
}
所以的确这样是可以手动触发 KVO 的。
3. 如果直接设置实例变量能触发 KVO 吗?
由于触发KVO是在属性的 set 方法里面做的操作,所以直接设置实例变量不能触发 KVO。
验证一下
直接设置 self.whiteCat->_age = 5;
// ------------------------- Cat -------------------------
@interface Cat : NSObject {
// 把它暴露出来
@public
NSInteger _age;
}
@property (nonatomic, assign) NSInteger age;
@end
// ------------------------- ViewController -------------------------
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 在这里测试
self.whiteCat->_age = 5;
}
// ... 其它代码
// ... 其它代码
// ... 其它代码
@end
编译运行,点击测试。发现没有触发 KVO。
4. 如果为多个对象的属性添加 KVO 那么 runtime 会创建多个派生子类吗?
这个其实不管对某个类的不同的实例对象添加多少属性 KVO,它们的 isa 指针指向的原有的派生子类都是一样的。这个可以通过 object_getClass: 方法验证一下就知道了。