美文网首页
底层原理:KVO

底层原理:KVO

作者: 飘摇的水草 | 来源:发表于2022-01-11 20:49 被阅读0次
    面试题
    • iOS用什么方法实现对一个对象的kvo,kvo的本质是什么?
      答:KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变
    举例

    我们用一个代码例子来总结原理,首先创建一个 MyPerson 类,里面只有一个 age 的属性:

    @interface MyPerson : NSObject
    @property (assign, nonatomic) int age;
    @end
    

    然后在外部控制器中我们用 MyPerson 声明两个示例对象 person1person2。只对 person1 添加 KOV 监听,然后通过打印监听前和监听后的内存地址,来看他们的 isa 指针指向是否有变化,并且打印 person1 和 person2 的类对象和元类对象进行对比,methodForSelector用于打印出方法实现。

    @interface ViewController ()
    @property (strong, nonatomic) MyPerson *person1;
    @property (strong, nonatomic) MyPerson *person2;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.person1 = [[MyPerson alloc] init];
        self.person1.age = 1;
        
        self.person2 = [[MyPerson alloc] init];
        self.person2.age = 2;
        
        NSLog(@"person1添加KVO监听之前 - %@ %@",
              object_getClass(self.person1),
              object_getClass(self.person2));
        NSLog(@"person1添加KVO监听之前 - %p %p",
              [self.person1 methodForSelector:@selector(setAge:)],
              [self.person2 methodForSelector:@selector(setAge:)]);
        
        // 给person1对象添加KVO监听
        NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
        
        NSLog(@"person1添加KVO监听之后 - %@ %@",
              object_getClass(self.person1),
              object_getClass(self.person2));
        NSLog(@"person1添加KVO监听之后 - %p %p",
              [self.person1 methodForSelector:@selector(setAge:)],
              [self.person2 methodForSelector:@selector(setAge:)]);
    
        NSLog(@"类对象 - %@ %@",
              object_getClass(self.person1),  // self.person1.isa
              object_getClass(self.person2)); // self.person2.isa
    
        NSLog(@"元类对象 - %@ %@",
              object_getClass(object_getClass(self.person1)), // self.person1.isa.isa
              object_getClass(object_getClass(self.person2))); // self.person2.isa.isa
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        // NSKVONotifying_MyPerson是使用Runtime动态创建的一个类,是MyPerson的子类
        // self.person1.isa == NSKVONotifying_MyPerson
        //[self.person1 setAge:21];
        self.person1.age = 20;
        self.person2.age = 20;
    }
    
    - (void)dealloc {
        [self.person1 removeObserver:self forKeyPath:@"age"];
    }
    

    当监听对象的属性值发生改变时,就会调用

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

    打印结果:

    person1添加KVO监听之前 - MyPerson MyPerson
    person1添加KVO监听之前 - 0x1078b6440 0x1078b6440
    person1添加KVO监听之后 - NSKVONotifying_MyPerson MyPerson
    person1添加KVO监听之后 - 0x7fff207bf79f 0x1078b6440
    类对象 - NSKVONotifying_MyPerson MyPerson
    元类对象 - NSKVONotifying_MyPerson MyPerson
    

    可以看出,在添加监听之前和之后methodForSelector打印出的内存地址发生了变化,可以在控制台里打印出这个内存对应的方法:

    (lldb) p (IMP)0x1078b6440
    (IMP) $0 = 0x000000000456789c0 (Interview01 ` -[MJPerson setAge:]) at MJPerson.m:13
    

    KVO本质分析

    我们发现添加KVO监听之前,person1 和 person2 的类对象是一样的都是 MyPerson,但是添加 KVO 监听之后,person1 的类对象变为了NSKVONotifying_MyPerson。

    未使用KVO监听的对象:

    使用了 KVO 监听的对象:

    _setAge 其实是调用了_NSSetIntValueAndNotify 方法,
    _NSSetIntValueAndNotify 方法内部其实是如下伪代码:

    // 伪代码
    void _NSSetIntValueAndNotify() {
        [self willChangeValueForKey:@"age"];
        [super setAge:age];
        [self didChangeValueForKey:@"age"];
    }
    
    - (void)didChangeValueForKey:(NSString *)key {
        // 通知监听器,某某属性值发生了改变
        [oberser observeValueForKeyPath:key ofObject:self change:nil context:nol];
    }
    

    Foundation 框架内除了有 _NSSetIntValueAndNotify,还有_NSSetBoolValueAndNotify_NSSetCharValueAndNotify_NSSetDoubleValueAndNotify 等等类型。

    _NSSetxxxValueAndNotify 的内部实现

    调用顺序如下:

    • 调用 willChangeValueForKey:
    • 调用原来的 setter 实现
    • 调用 didChangeValueForKey: didChangeValueForKey: 内部会调用 observer 的 observeValueForKeyPath:ofObject:change:context: 方法

    NSKVONotifying_MyPerson 除了重写了 setAge: 方法,还重写了 class(屏蔽内部实现,不让开发者知道)、dealloc、_isKVOA 方法。如果调用下面的代码:

    NSLog(@"%@ %@", object_getClass(self.person1), object_getClass(self.person2));
    NSLog(@"%@ %@", [self.person1 class]), [self.person2 class]);
    

    会发现打印为:

    NSKVONotifying_MyPerson  MyPerson
    MyPerson  MyPerson
    
    总结
    1. iOS用什么方法实现对一个对象的kvo,kvo的本质是什么?

      • 利用Runtime API动态生成一个子类,并且让instance对象的isa指向这个全新的子类
      • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
      • willChangeValueForKey:
      • 父类原来的Setter
      • didChangeValueForKey:
      • 内部会触发监听器(Observer)的监听方法(observeValueForKeyPath:ofObject:change:context)
    2. 如何手动触发KVO?

      • 手动调用willChangeValueForKey:和didChangeValueForKey:
    3. 直接修改成员变量会触发KVO吗?

      • didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context方法。
    • OC在运行过程中动态创建了一个类,OC让被监听的对象的isa指向了这个类的全新的子类,然后设置属性值时是调用了这个子类的set方法,这个set方法里调用了一个名为_NSSetIntValueAndNotify()的C函数,在这个C函数里又调用了[self willChangeValueForKey:@"age"][super setAge:age][self didChangeValueForKey:@"age"],然后在didChangeValueForKey里又通知了观察者,完整的子类代码实现如下:
    @implementation NSKVONotifying_MJPerson
    
    // 在`setAge:`方法中调用了`_NSSetIntValueAndNotify()`这个C语言函数
    - (void)setAge:(int)age
    {
       _NSSetIntValueAndNotify()
    }
    
    //伪代码
    void  _NSSetIntValueAndNotify()
    {
       [self willChangeValueForKey:@"age"];
       // 2.调用父类的`setAge:`函数
       [super setAge:age];
       [self didChangeValueForKey:@"age"];
    }
    
    - (void)didChangeValueForKey:(NSString *)key
    {
       // 调用监听器的监听方法,通知监听器某一个对象的属性值发生了改变
       [observer observeValueForKeyPath:key ofObject:self change:nil  context:nil];
    }
    @end
    
    • 并不是所有的属性都能监听的
    • 反编译工具:Hopper

    相关文章

      网友评论

          本文标题:底层原理:KVO

          本文链接:https://www.haomeiwen.com/subject/bepbcrtx.html