文章

KVO实现原理

什么是KVO

KVO全称KeyValueObserving,俗称键值监听,是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。

KVO实现原理

  • KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。

  • 在运行时根据原类创建一个中间类(假如这个类名为Person,那么这个中间类就为NSKVONotifying_Person),这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。当修改 instance 对象的属性时,会调用 Foundation框架的 _NSSetXXXValueAndNotify 函数 ,该函数里面会先调用 willChangeValueForKey: 然后调用父类原来的 setter 方法修改值,最后是 didChangeValueForKey:。didChangeValueForKey 内部会触发监听器(Oberser)的监听方法observeValueForKeyPath:ofObject:change:context:

  • 并且将class方法重写,返回原类的Class。

实践

  1. 首先创建一个Person类,继承自NSObject。

    Person.h (部分代码)

    1
    2
    3
    4
    5
    
     @interface Person : NSObject
        
     @property (nonatomic, assign) int age;
        
     @end
    
  2. 在控制器中初始化一个Person实例。

    ViewController.m(部分代码)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
     @interface ViewController ()
        
     @property (nonatomic, strong) Person *man;
        
     @end
        
     @implementation ViewController
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         // Do any additional setup after loading the view.
            
         Person *man = [Person new];
         _man = man;
         //断点1
         [man addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
        
         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
             man.age = 10;
         });
        
     }
        
     - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
     {
         NSLog(@"%@", object); //断点2
         NSLog(@"%@", keyPath);
         NSLog(@"%@", change);
     }
    

    在断点1和2分别打上断点,然后在lldb中输入:

    1
    
     p  _man->isa
    

    可以看到,分别打印了:

    断点1:

    1
    2
    
     (lldb) p _man->isa
     (Class) $1 = Person
    

    断点2:

    1
    2
    
     (lldb) p _man->isa
     (Class) $2 = NSKVONotifying_Person
    

    这说明了Runtime的isa-swizzling动态的生成了Person的子类:NSKVONotifying_Person,并且把_man的isa指向了NSKVONotifying_Person。

  3. 松开断点,可以看到控制台打印:

    1
    2
    3
    4
    5
    6
    7
    
     2020-05-13 17:00:13.258194+0800 KVO[37184:1734037] <Person: 0x6000016e2220>
     2020-05-13 17:00:13.258402+0800 KVO[37184:1734037] age
     2020-05-13 17:00:13.258636+0800 KVO[37184:1734037] {
         kind = 1;
         new = 10;
         old = 0;
     }
    
  4. 如果需要手动触发KVO,可以在Person.h加入一个成员变量:

    1
    2
    3
    4
    5
    6
    7
    8
    
     @interface Person : NSObject
     {
         @public NSString *_name;
     }
        
     @property (nonatomic, assign) int age;
        
     @end
    

    在ViewController.m中添加观察者:

    1
    
     [man addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    

    此时,修改_man->_name,

    1
    2
    3
    4
    
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
         man.age = 10;
         man->_name = @"Tom";
     });
    

    打印后可以发现,并没有监听到name的变化。

    手动触发:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
         man.age = 10;
            
         //不会触发KVO
         //man->_name = @"Tom";
            
         //手动触发
         [man willChangeValueForKey:@"name"];
         man->_name = @"Tom";
         [man didChangeValueForKey:@"name"];
     });
    
  5. 还可以通过KVC触发:

    1
    
     [man setValue:@"Tome" forKey:@"name"];
    
  6. 最后,要记得移除观察者,如果重复移除,将会导致崩溃。

    1
    2
    3
    4
    
     - (void)dealloc
     {
         [_man removeObserver:self forKeyPath:@"age"];
     }
    

参考资料:

关于KVO看这篇就够了

iOS开发 – KVO的实现原理与具体应用

本文由作者按照 CC BY 4.0 进行授权