Objective-C 里面的 KVO

322 阅读7分钟

看一个例子

#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;
}

可以看出触发了属性 ageKVO

看看添加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_CatCat 的子类。

打印一下 Cat 类的前两个变量 isa 指针和 superclass 指针的内存信息:

(lldb) x/2xg 0x1033b5690
0x1033b5690: 0x00000001033b5668 0x00007fff86d50660

可以看到 Catisa 指针地址指向和 NSKVONotifying_Catisa 指针地址指向不一样,也就是说它们的元类不一样。

画一下 whiteCat 添加 KVO 的变化

没有添加 KVO 的时候是这样的

1.png

虚线是对应的 isa 指针的指向,实线是对应的 superclass 指针的指向。

blackCatwhiteCat 都指向同一个类。

添加 KVO 以后是这样的

2.png

其中 NSKVONotifying_Cat 是类, MetaNSKVONotifying_Cat 是对应的元类。

Cat 是类, MetaCat 是对应的元类。

NSObject 是类, MetaNSObject 是对应的元类。

blackCatwhiteCat 是实例对象。

继承关系是这样的:NSKVONotifying_Cat: Cat: NSObject

所以添加 KVO 后发生了什么?

首先输出一下 CatNSKVONotifying_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_CatsetAge: 方法会执行类似上面的代码。

验证 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: 方法验证一下就知道了。