KVO原理分析

2020-10-27  本文已影响0人  奉灬孝

一、KVO底层实现原理

示例代码:

@interface LGViewController ()
@property (nonatomic, strong) LGPerson *person;
@end
@implementation LGViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[LGPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"实际情况:%@-%@",self.person.name);
    self.person.name = @"KC";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
@end

KVO 的实现过程实际上是利用了 OCruntime 机制,当一个实例对象(比如上面的 self.person)添加观察者时,底层根据该实例对象所属的类动态添加了一个类(动态添加的类名就是在原来类的类名前加上NSKVONotifying_前缀),这个类是继承自原来的类的。上面实例的底层实现过程如下:

self.person 添加观察者时,底层就利用 runtime 动态生成一个叫 NSKVONotifying_LGPerson 的类,这个类继承自 LGPerson 类,并重写了以下实例方法: 重写 class 方法,不重写的话调用这个方法返回的是 NSKVONotifying_LGPerson 这个类,重写后返回的是原本的LGPerson 类。苹果这么做的目的是为了隐藏 KVO 的实现细节。 重写 dealloc 方法,在这个方法里面做一些收尾的工作。 重写 _isKVOA 方法,这是一个私有方法,我们不必关心。 重写被监听属性的 setter 方法,上面案例只监听了 name 属性,所以只需重写 setName: 方法。重写 setter 是实现 KVO 的关键,在 setter 方法里面实际是调用的 Foundation 框架下的 _NSSet***ValueAndNotify 方法(***表示不是一个固定的,这个和监听的属性的类型有关,比如是属性是int类型的话这里就是 __NSSetIntValueAndNotify ,所包含的类型会在后面列出来)。
然后将 self.person 这个实例对象的 isa 改为指向 NSKVONotifying_LGPerson (原本是指向 LGPerson 类的)。
当我们设置被监听属性的值时 self.person.name = @"KC",是调用的 setName: 方法,前面说了 setName: 方法被重写了,所以实际上调用的是 _NSSetObjectValueAndNotify 这个方法。这个方法实现苹果是没有开源的,无法得知其具体实现,不过可以猜出其实现流程大致如下: 首先调用 [self willChangeValueForKey:@"name"]; 这个方法。 然后调用原先的 setter 方法的实现(比如 _name = name; ); 再调用 [self didChangeValueForKey:@"name"]; 这个方法。 最后在 didChangeValueForKey: 这个方法中调用观察者的 observeValueForKeyPath: ofObject: change: context: 方法来通知观察者属性值发生了变化。

二、 KVO底层实现的验证

2.1 我们怎么知道添加观察者时动态添加了一个类?

官方文档:

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.
当为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。 isa指针的值不一定反映实例的实际类

首先,我们来验证一下这个中间类,我们可以看到在设置 KVO 之后,对象的类已经指向了 NSKVONotifying_LGPerson

设置KVO前后打印类信息

KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class
KVO 给名为 NSKVONotifying_Person 的类分配空间失败,自动键值观察对于该类不会工作

2.2 如何知道重写了哪些方法?

这里我们需要用到 runtime 的一些 API 来获取一个类对象里面存储的方法列表信息,下面我们先封装一个方法来获取这些信息,然后把监听前和监听后的方法列表打印出来。

#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(@"%@",NSStringFromSelector(sel));
    }
    free(methodList);
}

没有设置KVO之前

self.person = [[LGPerson alloc] init];
[self printClassAllMethod:object_getClass(self.person)];

打印结果如下:

2020-10-27 20:14:43.272856+0800 003---自定义KVO[74222:1238967] copyWithZone:
2020-10-27 20:14:43.272988+0800 003---自定义KVO[74222:1238967] .cxx_destruct
2020-10-27 20:14:43.273091+0800 003---自定义KVO[74222:1238967] name
2020-10-27 20:14:43.273197+0800 003---自定义KVO[74222:1238967] setName:

设置KVO之后

[self.person lg_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClassAllMethod:object_getClass(self.person)];

打印结果如下:

2020-10-27 20:14:49.319536+0800 003---自定义KVO[74222:1238967] setName:

所以可以看出 KVO 的原理是生成一个中间类,重写监听属性的 setter 方法

三、KVO总结

KVO 的核心是动态生成一个继承自原类的类,然后将实例对象的 isa 指向这个类。然后重写了监听属性的 setter 方法,在原有 setter 方法的前面调用 willChangeValueForKey 方法,在原有 setter 方法的后面调用 didChangeValueForKey

所以我们要判断某个操作是否会触发 KVO 关键在于它是否调用了监听属性的 setter 方法。比如上面的例子,self.person.name = @"KC";这种方式就是调用 setter 方法,所以它会触发 KVO 。但是下面这几种方式是不会触发 KVO 的:

上一篇下一篇

猜你喜欢

热点阅读