iOS进阶之KVO底层原理

前言

KVO作为iOS一个设计模式,监听对象属性变化。通过属性变化来做出一些处理。那么KVO底层原理是什么?相信大家前期都不怎么关注过,知其然知其所以然,所以我也研究讲一下KVO底层实现原理。

思考

  1. KVO 底层实现是什么?
  2. 如何手动触发 KVO?
  3. 修改成员变量的值会触发 KVO 吗?
  4. KVC 赋值会触发 KVO 吗?

把这几个问题都整明白,KVO掌握的也就差不多了。

KVO的底层实现

首先创建一个对象Person,在Person.h声明一个属性

  1. 创建两个对象,一个对象KVO监听,一个对象不监听,查看一下他们的监听前后地址

     Person *p1 = [[Person alloc] init];
     Person *p2 = [[Person alloc] init];
     NSLog(@"KVO监听前:p1:%@ p2:%@", p1, p2);
    
     [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|   NSKeyValueObservingOptionNew context:nil];
     
     NSLog(@"KVO监听后:p1:%@ p2:%@",p1, p2);
    

打印的结果:

     KVO监听前:p1: p2:
     KVO监听后:p1: p2:

打印的结果说明监听前后地址没啥变化。

  1. 那就查看一下监听前后对象的类对象有没有变化,使用object_getClass来获取类对象,只有引入#import 才能使用object_getClass方法

     Person *p1 = [[Person alloc] init];
     Person *p2 = [[Person alloc] init];
     
     id class1 = object_getClass(p1);
     id class2 = object_getClass(p2);
     NSLog(@"KVO监听前:p1:%@ p2:%@",class1, class2);
     
     [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
     
     class1 = object_getClass(p1);
     class2 = object_getClass(p2);
    
     NSLog(@"KVO监听后:p1:%@ p2:%@",class1, class2);
    

    打印结果是:

     KVO监听前:p1:Person p2:Person
     KVO监听后:p1:NSKVONotifying_Person p2:Person
    

    发现监听后的p1是的类对象变成NSKVONotifying_Person,怎么监听后Person变成NSKVONotifying_Person。NSKVONotifying_Person是什么东东?

  2. 进一步窥探 KVO 添加前后的变化,打印 setName 方法实现IMP指针有没有发生改变,我们知道同一个方法的实现,IMP地址是不变的.

     Person *p1 = [[Person alloc] init];
     Person *p2 = [[Person alloc] init];
      //setName的IMP
     IMP imp1 = [p1 methodForSelector:@selector(setName:)];
     IMP imp2 = [p2 methodForSelector:@selector(setName:)];
     NSLog(@"KVO监听前:imp1:%p imp2:%p",imp1, imp2);
     
     [p1 addObserver:self forKeyPath:@"name" 
     options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    
     //setName的IMP
     imp1 = [p1 methodForSelector:@selector(setName:)];
     imp2 = [p2 methodForSelector:@selector(setName:)];
     NSLog(@"KVO监听后:imp1:%p imp2:%p",imp1, imp2);
    

    打印结果:

     KVO监听前:imp1:0x10fa01640 imp2:0x10fa01640
     KVO监听后:imp1:0x10fd5a63a imp2:0x10fa01640
    

    从打印结果来看连 setName方法都不一样了, 为了一探究竟 ,对上边的 NSKVONotifying_Person 和 添加 KVO 之后的 imp 指针进行进一步研究.首先 在 lldb 上输入 imp1和 imp2

     (lldb) po imp1
    
     (Foundation`_NSSetObjectValueAndNotify)
     
     (lldb) po imp2
     (KVO底层原理`-[Person setName:] at Person.h:13)
    

    发生了 imp1 方法实现在 Foundation 框架里的 _NSSetObjectValueAndNotify 函数中 ,而 imp2 则调用了 Person setName 方法.
    添加了 KVO 之后 p1 修改 name 值之后 不再调用 Person 的 setName方法 ,而 p2没有添加 kvo 监听 依然正常调用 setName:方法 ,由此可以得出 p1 添加完 KVO 监听后 系统修改了默认方法实现,那么既然没有调用 setName: 方法 为什么 p1.name 的值也发生了改变?

  3. NSKVONotifying_Person和 Person 之间的关系,打印一下各自的父类

      Person *p1 = [[Person alloc] init];
      Person *p2 = [[Person alloc] init];
    
     //获取p1 p2类型对象
     id class1 = object_getClass(p1);
     id class2 = object_getClass(p2);
     id superClass1 = class_getSuperclass(class1);
     id superClass2 = class_getSuperclass(class2);
    
     NSLog(@"KVO监听前:superclass1:%@ superclass2:%@",superClass1, superClass2);
    
     [p1 addObserver:self forKeyPath:@"name" 
     options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    
     class1 = object_getClass(p1);
     class2 = object_getClass(p2);
     superClass1 = class_getSuperclass(class1);
     superClass2 = class_getSuperclass(class2);
    
     NSLog(@"KVO监听后:superclass1:%@ superclass2:%@",superClass1, superClass2);
    

    打印结果:

     KVO监听前:superclass1:NSObject superclass2:NSObject
     KVO监听后:superclass1:Person superclass2:NSObject
    

    通过打印 NSKVONotifying_Person 的 superclass 和 Person 的 superclass 可以得出, NSKVONotifying_Person是一个 Person 子类,那么为什么苹果会动态创建这么一个 子类呢? NSKVONotifying_Person 这个子类 跟 Person 内部有哪些不同呢 ?

  4. 输出下 Person 和 NSKVONotifying_Person 内部的方法列表 和 属性列表 ,看看NSKVONotifying_Person 子类都添加了哪些方法和属性.

     Person *p1 = [[Person alloc] init];
     Person *p2 = [[Person alloc] init];
    
     //获取p1 p2类型对象
     id class1 = object_getClass(p1);
     id class2 = object_getClass(p2);
    
     [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    
     class1 = object_getClass(p1);
     class2 = object_getClass(p2);
     NSString *methodList1 = [self printPersonMethods:class1];
     NSString *methodList2 = [self printPersonMethods:class2];
    
     NSLog(@"KVO监听后:methodlist1:%@ \n methodlist2:%@",methodList1, methodList2);
    
     - (NSString *) printPersonMethods:(id)obj {
    
         unsigned int count = 0;
         Method *methods = class_copyMethodList([obj class],&count);
         NSMutableString *methodList = [NSMutableString string];
         [methodList appendString:@"[\n"];
         for (int i = 0; i

打印结果:

KVO监听后:methodlist1:[
    setName:
    class
    dealloc
    _isKVOA
    ] 
    methodlist2:[
    .cxx_destruct
    name
    setName:
    ]
  • 发现监听后的methodlist1方法内部也有setName:
  • 重写了class 和 dealloc 方法,多了个_isKVOA方法。
  • KVO 在实现中通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类。
  • 重写了 setName 方法 ,那么 setName 内部一定是做了注册通知的事情,当值改变才会触发 observeValueForKeyPath 监听方法.

继续探究 NSKVONotifying_Person 子类 重写 setName 都做了什么?

  • 前面打印了监听后p1的setName的IMP方法变成(Foundation`_NSSetObjectValueAndNotify)
  • setName方法内部应该是调用了 Foundation 的_NSSetObjectValueAndNotify 函数
  • 在 _NSSetObjectValueAndNotify实现触发监听
    • 首先会调用 willChangeValueForKey
    • 然后给 name 属性赋值
    • 最后调用 didChangeValueForKey
    • 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .

由于源码无法查看,大概设想一下NSKVONotifying_Person的实现代码:

- (void)setName:(NSString *)name {

    _NSSetObjectValueAndNotify()
}

- (void)willChangeValueForKey:(NSString *)key {
    
    [super willChangeValueForKey:key];
}

- (void)didChangeValueForKey:(NSString *)key {

    [super didChangeValueForKey:key];
}

void _NSSetObjectValueAndNotify() {
    
    //触发监听
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];]
}

所以我们依然可以通过重写Person 的 willChangeValueForKey 和 didChangeValueForKey 验证我们的猜想.打断点调试一下

    //每次属性改变,第二步
    - (void)setName:(NSString *)name {
        
        _name = [name copy];
    }
    
    //每次属性改变,先走这一步
    - (void)willChangeValueForKey:(NSString *)key {

        [super willChangeValueForKey:key];
    }

    //每次属性改变,第三步
    - (void)didChangeValueForKey:(NSString *)key {

        [super didChangeValueForKey:key];
    }
    
    //这个方法不是在Person,是在哪个类监听的p1
    //每次属性值改变,第四步会走observeValueForKeyPath
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

NSLog(@"new -- %@",change[NSKeyValueChangeNewKey]);
    }
  • 监听时候Runtime动态的创建Person的子类NSKVONotifying_Person
  • 重写了setName方法
  • 重写class方法返回的还是Person类
  • 重写了dealloc 和 class 方法 是为了做一些KVO 释放内存和隐藏外界对于NSKVONotifying_Person 子类的存在
  • 这就是我们打印[p1 class] [p2 class]都显示Person,让我们误以为 Person 没有发生变化 。
iOS进阶之KVO底层原理_第1张图片
KVO实现原理图.png

问题解答

  1. 如何手动的触发KVO?

    • 关闭该属性的自动触发

    • 实现willChangeValueForKey didChangeValueForKey成对出现才能手动触发KVO

    • 一个被观察属性发生改变之前,willChangeValueForKey: 一定会被调用,这就会记录旧的值

    • 当执行didChangeValueForKey这个方法就会触发监听,这个会记录新的值

        - (void)setName:(NSString *)name            {
      
            if ([_name isEqualToString:name]) return;
            //手动触发 关闭自动监听之后,如果不写下面三行代码是不会触发监听的,
            //要是打开自动监听的话,willChangeValueForKey didChangeValueForKey可以不用不写的,系统会自动加上
            [self willChangeValueForKey:@"name"];
            _name = name;
            [self didChangeValueForKey:@"name"];
        }
      
        + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
      
            //对name属性关闭自动监听
            if ([key isEqualToString:@"name"]) {
      
                return NO;
            } else {
                //对其他属性没影响
                return [super automaticallyNotifiesObserversForKey:key];
            }
        }
      
  2. 修改成员变量的值会触发 KVO 吗?

    • 修改成员变量不会触发,因为不会触发setter方法,因此不会触发KVO相关的代码
  3. KVC 赋值会出发 KVO 吗?

    • 会触发KVO,KVC 对属性赋值时候 是会在这个类里边去查找setName、_name、_isName、name、isName等方法的
    • 最终会调用属性的setter方法,所以KVO还是会被触发的.
    • KVO在调用存取方法之前总是调用 willChangeValueForKey ,之后总是调用 didChangeValueForkey: 怎么做到的呢?答案是通过 isa 混写(isa-swizzling)

使用KVO注意问题

  • 如果同一个属性在不同的类(比如父类)均设置了KVO,那么可通过定义不同的context(类名)来区分,并且相应移除。
  • 对同一个KVO属性多次移除,会导致程序crash。
  • 使用类别(category)向一个已存在的类添加一个属性,可以用KVO观察其变化。
  • 我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context为@"ThisIsMyKVOContextNotSuper";然后在dealloc中remove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的kvo,而不是父类中的kvo,避免二次remove造成crash。

你可能感兴趣的:(iOS进阶之KVO底层原理)