弄透KVO

目标

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

KVO的本质是什么?

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

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

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

如何手动触发KVO?

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

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

因此,想要手动触发KVO,能够经过手动调用willChangeValueForKey:didChangeValueForKey: 来实现,须要强调这两个方法都必须调用才会起做用bash

如何手动实现KVO?

手动实现KVO的过程就是把系统实现KVO的过程本身用代码实现一下。大体流程以下:app

  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,键值监听。 ui

基本用法

  • 注册观察者,实施监听
[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值发生改变后通知监听者。spa

重写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后,打印p一、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: 方法。
相关文章
相关标签/搜索