KVO底层原理

概念

KVO的全称是Key-Value- Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。

本质

  • 利用RuntimeAPI动态生成一个子类,并且让改instance对象的isa指向这个全新的子类
  • 当修改对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
    1.先调用willChangeValueForKey:
    2.调用父类的setter方法
    3.调用didChangeValueForKey:
  • 内部触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)

底层原理探索

先看以下一段简单的KVO的代码实现,观察RMPerson的属性age改变,简单的代码实现

#import "ViewController.h"
#import "RMPerson.h"

@interface ViewController ()
@property (nonatomic, strong) RMPerson *person1;
@property (nonatomic, strong) RMPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.person1 = [[RMPerson alloc] init];
    self.person1.age = 10;

    self.person2 = [[RMPerson alloc] init];
    self.person2.age = 20;
    
    // 给person对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.person1 setAge:11];
    [self.person2 setAge:21];
}

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

- (void)dealloc
{
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

@end

--------------------------------------------------------------------------------------------------------------------------
//RMPerson.h
#import 
@interface RMPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@end

//RMPerson.M
#import "RMPerson.h"
@implementation RMPerson
- (void)setAge:(int)age
{
    _age = age;
}
@end

在以上代码中,对象person1的属性age添加监听person2的属性age没有监听作对比,可以猜测,两者调用的都是对象的setAge:方法,为何添加了KVO监听的peron1会回调方法observeValueForKeyPath:ofObject:change:context:,而person2却没有。
猜测:

  • 1.苹果调用setAge:上做了手脚
  • 2.会不会是生成了一个新的中间对象或者利用OC独有的运行时状态,生成了一个RMPerson的子类,重写了age的setter方法?

带着上面两点疑问,打印下监听前后person1person2的类对象是否有变化,看以下代码

1、监听前后类对象的变化
    NSLog(@"监听前 %@:%p   %@:%p",
          object_getClass(self.person1),object_getClass(self.person1),
          object_getClass(self.person2),object_getClass(self.person2));
    
    // 给person对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
    NSLog(@"监听后 %@:%p   %@:%p",
          object_getClass(self.person1),object_getClass(self.person1),
          object_getClass(self.person2),object_getClass(self.person2));

//打印结果
2018-08-24 17:40:19.689016+0800 KVO[60640:4402657] 监听前 RMPerson:0x10e2600b0   RMPerson:0x10e2600b0
2018-08-24 17:40:20.621251+0800 KVO[60640:4402657] 监听后 NSKVONotifying_RMPerson:0x608000112750   RMPerson:0x10e2600b0

从上面代码看出,
1.监听前两者都是RMPerson类,其类对象地址是一致的
2.给对象person1的属性age添加监听后,对象person1的打印的类是NSKVONotifying_RMPerson
所以,我们可以得出给对象属性添加监听后,苹果会动态的生成一个NSKVONotifying_XXX的中间类,其继承于RMPerson,如何得知呢?打印一下NSKVONotifying_RMPerson的父类 [object_getClass(self.person1) superClass]即可知道其父类是RMPerson。

2、监听前后其"setAge:方法地址"

打印下添加监听后其setAge:方法地址

 NSLog(@"person1添加KVO监听之后 - %p %p",
        [self.person1 methodForSelector:@selector(setAge:)],
        [self.person2 methodForSelector:@selector(setAge:)]);

2018-08-27 11:02:25.651418+0800 KVO[72155:5132928] person1添加KVO监听之后 - 0x100669f8e 0x1002c4520
(lldb) p (IMP)0x100669f8e
(IMP) $0 = 0x0000000100669f8e (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP)0x1002c4520
(IMP) $1 = 0x00000001002c4520 (KVO`-[RMPerson setAge:] at RMPerson.m:13)
(lldb) 

从上面,打印后,使用lldb打印其地址的内容,可以得知,监听对象person1的属性age后,其setAge:方法底层实际是调用了_NSSetIntValueAndNotify方法,此方法的伪代码如下

- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}

// 伪代码
void _NSSetIntValueAndNotify()
{
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
    // 通知监听器,某某属性值发生了改变
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}

要了解其_NSSetIntValueAndNotify其方法的实现,要反编译其Foundation框架的底层实现,有兴趣的自己再了解。从上面我们可看出,setAge:方法实际实现了此三个方法
① willChangeValueForKey:
② [super setAge:age]
③ didChangeValueForKey
最后再通知监听器,告诉其某某属性值发生改变了,回调了[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];此方法。

总结

使用KVO监听了某个对象的属性,实际就是

  • 利用RuntimeAPI动态生成一个子类,并且让改instance对象的isa指向这个全新的子类
  • 当修改对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
    1.先调用willChangeValueForKey:
    2.调用父类的setter方法
    3.调用didChangeValueForKey:
  • 内部触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
    为何称KVO为黑魔法,实际上掩饰了利用RuntimeAPI动态生成一个子类,属性值发生了改变时,修改父类的值得过程

你可能感兴趣的:(KVO底层原理)