KVO底层原理

2019-06-27  本文已影响0人  Goose的小黄花

KVO的全称是Key-Value Observing,俗称键值监听,可以用于监听某个对象属性值的改变。
下面我们来了解一下KVO的基本使用。

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

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[Person alloc] init];
    self.person2.age = 2;
    
    //给person对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    //    [self.person2 addObserver:self forKeyPath:@"age" options:options context:@"456"];
}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object,keyPath,change,context);
}

//移除监听者
-(void)dealloc{
    [self.person1 removeObserver:self forKeyPath:@"age"];
//    [self.person2 removeObserver:self forKeyPath:@"age"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    self.person1.age = 20;
    self.person2.age = 30;
}

@end

打印结果
监听到<Person: 0x600003f74540>的age属性值改变了 - {
    kind = 1;
    new = 20;
    old = 1;
} - 123

上述代码中,我们为person1的对象的age属性添加了观察者,实现了- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法,当person1的age属性值发生变化时,在该方法中监听到我们属性值的变化。而person2没有添加观察者,属性值发生变化时,没有监听到。

探寻KVO底层实现原理

通过上述代码我们发现,一旦age属性值发生了改变,就会通知观察者,并且我们知道赋值操作都是调用了属性的set方法。我们到Person类中重写了age的set方法,观察是否是KVO在set方法内部做了一些操作。

结果是person1和person2对象调用同样的set方法,但person1对象除了调用set方法之外还会执行观察者的observeValueForKeyPath方法。同样是调用了setAge方法,那为什么结果不一样呢?在调用方法一致的情况下,差异应该是出现在对象本身上,person1对象本身在底层肯定发生了变化,KVO应该是对person1做了一些不为人知的事情。下面我们就去探索一下究竟发生了什么变化。

本质分析

首先我们在上述代码touchedBegan方法中,给age赋值的代码打上断点,观察一下person1在addObserver后,发生了什么改变,我们通过打印isa指针,如下图所示

图1.addObserver后person1对象变化

通过上图我们发现,person1对象执行addObserver操作之后,person1对象的isa指针由之前的指向类对象Person变为指向NSKVONotifying_Person类对象,并且NSKVONotifying_Person类的superclass指针指向Person类,说明NSKVONotifying_Person类为Person类的子类,而person2对象没有发生任何改变。也就是说一旦person1对象添加KVO监听之后,其isa指针就会发生变化,因此set方法的执行效果就不一样了。

我们来看两张图,分别代表person2和person1在内存中存储的情况,用来做对比。


图2.未使用KVO监听的对象
图3.使用了KVO监听的对象

首先分析未使用KVO的对象person2,其isa指向的是类对象Person,Person中保存着person2的set和get方法,当对person2的age赋值时,直接找到Person类中保存的setAge方法进行调用,不会监听到值的变化。
而使用了的KVO的person1对象,其isa指向了一个新的类NSKVONotifying_Person,新的类中也创建了setAge方法。当对person1的age赋值时,就找到NSKVONotifying_Person类中的setAge方法并调用,监听值变化是在这个方法中实现的。系统通过runtime动态的创建了NSKVONotifying_Person类,并且NSKVONotifying_Person类继承至Person类,这就是为什么person1的isa指向新的类之后,我们之前在Person中重写了set方法也同样会调用。

接下来我们只需要弄清楚,NSKVONotifying_Person类中的setAge方法具体做了哪些事情。
进过分析,NSKVONotifying_Person中的setAge方法中其实调用了 Fundation框架中C语言函数 _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];
}
本质验证

在最开始给的实例代码中,给person1对象添加观察者之前之后分别打印person1和person2的setAge方法对应的地址。

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监听之后 - %p %p",
          [self.person1 methodForSelector:@selector(setAge:)],
          [self.person2 methodForSelector:@selector(setAge:)]);

打印结果
2019-06-26 17:09:00.276025+0800 KVO原理[90398:25370931] person1添加KVO监听之前 - 0x10f5a7530 0x10f5a7530
2019-06-26 17:09:02.655540+0800 KVO原理[90398:25370931] person1添加KVO监听之后 - 0x10f9023d2 0x10f5a7530
(lldb) p IMP(0x10f5a7530)
(IMP) $0 = 0x000000010f5a7530 (KVO原理`-[Person setAge:] at Person.m:13)
(lldb) p IMP(0x10f9023d2)
(IMP) $1 = 0x000000010f9023d2 (Foundation`_NSSetIntValueAndNotify)
(lldb) 

添加观察者之前,两个setAge方法对应的地址是一样的,添加观察者之后person1的setAge方法地址发生了变化。打印person2的setAge方法,显示的是调用Person类中的方法;打印person1的setAge方法,此时显示是调用了Foundation中的C语言函数_NSSetIntValueAndNotify

Foundation框架会根据属性的类型,调用不同的方法。例如我们之前定义的int类型的age属性,调用的是_NSSetIntValueAndNotify函数,那我们吧age的属性类型变为double再重新打印一遍。

打印结果
2019-06-26 17:42:41.731276+0800 KVO原理[90733:25452474] person1添加KVO监听之前 - 0x1023e0500 0x1023e0500
2019-06-26 17:42:42.430932+0800 KVO原理[90733:25452474] person1添加KVO监听之后 - 0x10273b18c 0x1023e0500
(lldb) p IMP(0x1023e0500)
(IMP) $0 = 0x00000001023e0500 (KVO原理`-[Person setAge:] at Person.m:13)
(lldb) p IMP(0x10273b18c)
(IMP) $1 = 0x000000010273b18c (Foundation`_NSSetDoubleValueAndNotify)
(lldb) 

我们发现调用的函数变为了_NSSetDoubleValueAndNotify,那么我们猜测Foundation框架中应该有许多地类型的函数,通过不同的类型调用不同的函数。
通过一些反编译手段,我们可以找到Foundation框架文件,通过执行命令行查询关键字找到相关函数。

图4.查看NSSetValueAndNotify的相关函数

_NSSetIntValueAndNotify内部的调用顺序又是怎样的呢?我们继续验证。
因为不能直接看到Foundation框架中的方法实现,我们可以通过父类中重写相关方法,也能看出方法的调用顺序。

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

@implementation Person

- (void)setAge:(int)age{
    _age = age;
    NSLog(@"setAge:");
}

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

- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey - begin");
    
    [super didChangeValueForKey:key];
    
    NSLog(@"didChangeValueForKey - end");
}

@end

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;
    
    //给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}

// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object,keyPath,change,context);
}

//移除监听者
-(void)dealloc{
    [self.person1 removeObserver:self forKeyPath:@"age"];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person1.age = 20;
}

打印结果
2019-06-26 18:18:22.365864+0800 KVO原理[91127:25550178] willChangeValueForKey
2019-06-26 18:18:22.366283+0800 KVO原理[91127:25550178] setAge:
2019-06-26 18:18:22.366337+0800 KVO原理[91127:25550178] didChangeValueForKey - begin
2019-06-26 18:18:22.366449+0800 KVO原理[91127:25550178] 监听到<Person: 0x600003ee8d10>的age属性值改变了 - {
    kind = 1;
    new = 20;
    old = 20;
} - 123
2019-06-26 18:18:22.366573+0800 KVO原理[91127:25550178] didChangeValueForKey - end

由以上打印结果可以看出,最先调用willChangeValueForKey方法,再调用setAge:方法,最后调用didChangeValueForKey已经改变方法,didChangeValueForKey中会调用监听器的监听方法。

如何证明NSKVONotifyin_Person内部结构如上图3所示呢?
我们通过runtime分别打印Person类对象和NSKVONotifying_Person类对象存储的对象方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person1 = [[MJPerson alloc] init];
    self.person1.age = 1;
    
    self.person2 = [[MJPerson alloc] init];
    self.person2.age = 2;
    
    // 给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    
    [self printMethodNamesOfClass:object_getClass(self.person1)];
    [self printMethodNamesOfClass:object_getClass(self.person2)];
}

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

打印结果
2019-06-27 15:18:33.343736+0800 KVO原理[2714:27649615] Person setAge:, age,
2019-06-27 15:18:33.343855+0800 KVO原理[2714:27649615] NSKVONotifying_Person setAge:, class, dealloc, _isKVOA,

通过上述代码的打印结果,我们发现NSKVONotifying_Person类中有4个对象方法。分别为setAge:,class, dealloc, _isKVOA。至此可以验证图上所示结构。
我们分析以下系统为什么要生成这几个方法

我们在person1添加过KVO监听之后,分别打印person1和person2对象的class可以发现他们都返回Person。

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

打印结果 Person,Person

如果NSKVONotifying_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到NSObject,而NSObject的class的实现大致为返回自己isa指向的类,返回person1的isa指向的类那么打印出来的类就是NSKVONotifying_Person,但是Apple不希望将NSKVONotifying_Person类暴露出来,并且不希望我们知道NSKVONotifying_Person内部实现,所以在内部重写了class类,直接返回Person类,那么外界在调用person1的class对象方法时,打印是Person类。这样person1给外界的感觉person1还是Person类,并不知道NSKVONotifying_Person子类的存在。

那么我们可以猜测NSKVONotifying_Person内重写的class内部实现大致为

- (Class) class {
     // 得到类对象,在找到类对象父类
     return class_getSuperclass(object_getClass(self));
}

面试题

1.iOS用什么方法实现对一个对象的KVO?(KVO的本质是什么?)
利用Runtime动态的生成一个instance对象原本类的子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSSet***ValueAndNotify函数

  • willChangeValueForKey:
  • 调用父类原来的set方法
  • didChangeValueForKey:
  • 内部会出发监听器(Observer)的监听方法observeValueForKeyPath:ofObject:change:context

2.如何手动触发KVO?
想手动触发KVO,我们需要自己调用willChangeValueForKey:didChangeValueForKey:两个方法,两个方法缺一不可。

代码验证

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[Person alloc] init];
    self.person1.age = 1;

    //给person1对象添加KVO监听
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
    
    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];
    
    [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);
}

打印结果
2019-06-27 16:06:59.103817+0800 KVO原理[3176:27785539] 监听到<Person: 0x6000024f87a0>的age属性值改变了 - {
    kind = 1;
    new = 1;
    old = 1;
} - 123

通过打印我们可以发现,age值没有发生变化的情况下,通过手动调用willChangeValueForKey: 和didChangeValueForKey:方法,内部成功调用了observeValueForKeyPath:ofObject:change:context:方法。

3.直接修改成员变量的值会触发KVO么?
因为直接修改成员变量的值不会调用set方法,所以就不会触发KVO

上一篇下一篇

猜你喜欢

热点阅读