弄透KVO

825 阅读5分钟

目标

  1. KVO的本质是什么?
  2. 如何手动触发KVO?
  3. 如何自己动手实现KVO?
  4. 直接修改成员变量会触发KVO吗?

KVO的本质是什么?

想要回答这个问题,首先需要弄明白当给一个对象的属性添加KVO后,系统做了哪些事情?

  • 当给一个对象的属性添加KVO监听时,系统会利用Runtime动态创建一个类,这个类是对象父类的子类
  • 将对象的isa指针由父类更改到新建的子类
  • 重写子类的setter方法,在子类的setter方法中添加触发KVO监听者的监听方法的机制

所以,KVO的本质是修改属性的setter方法,在属性的setter方法里添加调用监听方法的逻辑,为了不破坏原始类,系统又增加了动态创建子类并修改对象的isa指针的机制。

如何手动触发KVO?

想要知道如何手动触发KVO,首先需要弄明白系统是如何修改setter方法以调用监听方法的。

  • 在子类的setter方法会调用 _NSSetXXXValueAndNotify 函数
  • _NSSetXXXValueAndNotify 函数内部会调用 willChangeValueForKey: 和 didChangeValueForKey: 方法
  • didChangeValueForKey: 内部会调用KVO监听者的监听方法

所以,想要手动触发KVO,可以通过手动调用willChangeValueForKey:didChangeValueForKey: 来实现,需要强调这两个方法都必须调用才会起作用

如何手动实现KVO?

手动实现KVO的过程就是把系统实现KVO的过程自己用代码实现一下。大致流程如下:

  1. 添加一个NSObject分类,在分类中添加addObserver和removeObserver方法
  2. 判断监听的对象是否含有对应的key的setter方法
  3. 判断添加了前缀的子类是否已经存在
  4. 利用runtime的 objc_allocateClassPair 函数动态创建子类
  5. 判断KVO类有没有重写过setter方法
  6. 利用runtime的 class_addMethod 函数重写setter方法
  7. 重写的setter方法内部调用原始类的setter方法
  8. 重写的setter方法内部找到监听者进行回调
  9. 监听者的保存方式利用runtime的关联对象给分类添加一个数组属性

详细内容可参考iOS_KVO_Study

直接修改成员变量会触发KVO吗?

不会触发,直接修改成员变量并不会触发setter方法,因此也就不会触发KVO

探究KVO原理

KVO全称 Key-Value Observing,键值监听。

基本用法

  • 注册观察者,实施监听
[p1 addObserver:self
             forKeyPath:@"age"
             options:NSKeyValueObservingOptionNew
             context:nil];
  • 在回调方法里处理属性发生变化后的后续处理
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
  //...实现监听处理
}
  • 移除观察者
[self removeObserver:self forKeyPath:@“age"];

在添加监听后,age属性的值在发生改变时就会通知监听者,执行监听者的observeValueForKeyPath方法。接下来我们就一步步探究为何会在age值发生改变后通知监听者。

重写set方法

赋值操作会调用set方法,我们通过重写Person类的setAge:方法,观察是否是KVO在set方法内部做了一些操作来通知监听者。

// ViewController类
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;
    // self 监听 p1的 age属性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"监听到%@的%@改变了%@", object, keyPath,change);
}

// Person类
- (void)setAge:(NSInteger)age {
    _age = age;
}

通过观察发现p1和p2同样调用了setAge:方法,p1除了调用setAge:方法外还会执行监听者的observeValueForKeyPath方法。显然这些并不是在setAge:方法中调用的。

对比p1在addObserver前后的变化

既然不是通过修改setAge:方法来实现监听的,那addObserver方法对p1对象做了什么特殊处理呢?我们通过打印isa指针来进行对比。

kvo-po-isa.png

通过上图我们发现,p1对象在执行过addObserver后,isa指针发生了改变,由之前的Person变为了NSKVONotifying_Person。所以,系统生成的新类的格式是 NSKVONotifying_原类

思路验证

  • 打印方法实现的地址来看p1和p2的setAge:方法实现的地址在添加KVO前后有什么变化
NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)], [p2 methodForSelector:@selector(setAge:)]);
    
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
[self printMethods:object_getClass(p2)];
[self printMethods:object_getClass(p1)];
    
NSLog(@"添加KVO监听之后 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)], [p2 methodForSelector:@selector(setAge:)]);

kvo-p-address.png

通过打印的地址信息,我们发现在添加KVO监听之前,p1和p2的setAge:方法实现的地址相同,而经过KVO监听之后,p1的setAge方法实现的地址发生了变化,p1的setAge:方法的实现转换为了C语言的Foundation框架的 _NSSetLongLongValueAndNotify 函数。

  • 查看 NSKVONotifying_Person 的内部结构
    我们通过runtime分别打印Person类对象和NSKVONotifying_Person类对象内存储的对象方法
- (void)printMethods:(Class)cls {
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0; i < count; i++) {
        Method method = methods[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        
        [methodNames appendString:methodName];
        [methodNames appendString:@" "];
    }
    
    NSLog(@"%@", methodNames);
    free(methods);
}

打印内容如下:

kvo-print-methods.png

通过打印结果我们发现NSKVONotifying_Person中有4个对象方法,分别为 setAge:  class  dealloc  _isKVOA ,下图是NSKVONotifying_Person的内存结构以及方法调用顺序。

这里NSKVONotifying_Person重写class方法是为了隐藏NSKVONotifying_Person。我们在p1添加KVO后,打印p1、p2对象的class可以发现他们都返回Person。

NSLog(@"%@, %@", [p1 class], [p2 class]);
// 打印结果: Person, Person

猜测NSKVONotifying_Person内重写的class内部实现大致为:

- (Class) class {
     // 得到类对象,在找到类对象父类
     return class_getSuperclass(object_getClass(self));
}
  • 验证didChangeValueForKey:内部会调用observer的observerValueForKeyPath:ofObject:change:context:方法
    在Person类中重写willChangeValueForKey:和didChangeValueForKey:方法,模拟KVO的实现。
- (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");
}

打印结果:

kvo-changeforkey.png

通过打印的结果不难发现,确实在 didChangeValueForKey 方法内部调用了 observeValueForKeyPath:ofObject:change:context: 方法。