【OC底层】KVO原理

KVO的原理是什么?底层是如何实现的?

KVO是Key-value observing的缩写。

KVO是Objective-C是使用观察者设计模式实现的。

Apple使用了isa混写(isa-swizzling)来实现KVO。

 

我们可以通过代码去探索一下。

创建自定义类:XGPerson

@interface XGPerson : NSObject

@property (nonatomic,assign) int age;

@property (nonatomic,copy) NSString* name;

@end

我们的思路就是看看对象添加KVO之前和之后有什么变化,是否有区别,代码如下:

@interface ViewController ()

@property (strong, nonatomic) XGPerson *person1;
@property (strong, nonatomic) XGPerson *person2;

@end

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

    // 添加监听之前,获取类对象,通过两种方式分别获取 p1 和 p2的类对象
    NSLog(@"before getClass--->> p1:%@  p2:%@",object_getClass(self.person1),object_getClass(self.person2));
    NSLog(@"before class--->> p1:%@  p2:%@",[self.person1 class],[self.person2 class]);
        
    // 添加KVO监听
    NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];

    // 添加监听之后,获取类对象
    NSLog(@"after getClass--->> p1:%@  p2:%@",object_getClass(self.person1),object_getClass(self.person2));
    NSLog(@"after class--->> p1:%@  p2:%@",[self.person1 class],[self.person2 class]);
}

输出:

2018-11-02 15:16:13.276167+0800 KVO原理[4083:170379] before getClass--->> p1:XGPerson  p2:XGPerson
2018-11-02 15:16:13.276271+0800 KVO原理[4083:170379] before class--->> p1:XGPerson  p2:XGPerson


2018-11-02 15:16:13.276712+0800 KVO原理[4083:170379] after getClass--->> p1:NSKVONotifying_XGPerson  p2:XGPerson
2018-11-02 15:16:13.276815+0800 KVO原理[4083:170379] after class--->> p1:XGPerson  p2:XGPerson

从上面可以看出,object_getClass 和 class 方式分别获取到的 类对象竟然不一样,在对象添加了KVO之后,使用object_getClass的方式获取到的对象和我们自定义的对象不一样,而是NSKVONotifying_XGPerson,可以怀疑 class 方法可能被篡改了.

最终发现NSKVONotifying_XGPerson是使用Runtime动态创建的一个类,是XGPerson的子类.

看完对象,接下来我们来看下属性,就是被我们添加了KVO的属性age,我们要触发KVO回调就是去给age设置个值,那它肯定就是调用setAge这个方法.

下面监听下这个方法在被添加了KVO之后有什么不一样.

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


    // 添加KVO监听
    NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];

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

输出:

2018-11-02 15:16:13.276402+0800 KVO原理[4083:170379] person1添加KVO监听之前 - 0x10277c3e0 0x10277c3e0

2018-11-02 15:16:17.031319+0800 KVO原理[4083:170379] person1添加KVO监听之后 - 0x102b21f8e 0x10277c3e0

看输出我们能发现,在监听之前两个对象的方法所指向的物理地址都是一样的,添加监听后,person1对象的setAge方法就变了,这就说明一个问题,这个方法的实现变了,我们再通过Xcode断点调试打印看下到底调用什么方法

断点后,在调试器中使用 po 打印对象

(lldb) po [self.person1 methodForSelector:@selector(setAge:)]

  (Foundation`_NSSetIntValueAndNotify)

 

(lldb) po [self.person2 methodForSelector:@selector(setAge:)]

  (KVO原理`-[XGPerson setAge:] at XGPerson.m:13)

通过输出结果可以发现person1的setAge已经被重写了,改成了调用Foundation框架中C语言写的 _NSSetIntValueAndNotify 方法,

还有一点,监听的属性值类型不同,调用的方法也不同,如果是NSString的,就会调用 _NSSetObjectValueAndNotify 方法,会有几种类型

大家都知道苹果的代码是不开源的,所以我们也不知道 _NSSetIntValueAndNotify 这个方法里面到底调用了些什么,那我们可以试着通过其它的方式去猜一下里面是怎么调用的。

KVO底层的调用顺序

我们先对我们自定义的类下手,重写下类里面的几个方法:

类实现:

#import "XGPerson.h"

@implementation XGPerson

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

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

重写上面3个方法来监听我们的值到底是怎么被改的,KVO的通知回调又是什么时候调用的

我们先设置KVO的监听回调

// KVO监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionaryid> *)change context:(void *)context{
    
    NSLog(@"监听到%@的%@属性值改变了 - %@", object, keyPath, change[@"new"]);
}

 

我们直接修改person1的age值,触发一下KVO,输出如下:

2018-11-02 15:38:24.788395+0800 KVO原理[4298:186471] willChangeValueForKey
2018-11-02 15:38:24.788573+0800 KVO原理[4298:186471] XGPerson setAge
2018-11-02 15:38:24.788696+0800 KVO原理[4298:186471] didChangeValueForKey - begin
2018-11-02 15:38:24.788893+0800 KVO原理[4298:186471] 监听到0x60400022f420>的age属性值改变了 - 2
2018-11-02 15:38:24.789014+0800 KVO原理[4298:186471] didChangeValueForKey - end

从结果中可以看出KVO是在哪个时候触发回调的,就是在 didChangeValueForKey 这个方法里面触发的

NSKVONotifying_XGPerson子类的研究

接下来我们再来研究下之前上面说的那个 NSKVONotifying_XGPerson 子类,可能大家会很好奇这里面到底有些什么东西,下面我们就使用runtime将这个子类的所有方法都打印出来

我们先写一个方法用来打印一个类对象的所有方法,代码如下:

// 获取一个对象的所有方法
- (void)getMehtodsOfClass:(Class)cls{
    
    unsigned int count;
    Method* methods = class_copyMethodList(cls, &count);
    
    NSMutableString* methodList = [[NSMutableString alloc]init];
    for (int i=0; i < count; i++) {
        Method method = methods[i];
        NSString* methodName = NSStringFromSelector(method_getName(method));
        [methodList appendString:[NSString stringWithFormat:@"| %@",methodName]];
    }
    NSLog(@"%@对象-所有方法:%@",cls,methodList);

   // C语言的函数是需要手动释放内存的喔
   free(methods);

}

下面使用这个方法打印下person1的所有方法,顺便我们再对比下 object_getClass 和 class

    // 一定要使用 object_getClass去获取类对象,不然获取到的不是真正的那个子类,而是XGPperson这个类
    [self getMehtodsOfClass:object_getClass(self.person1)];

   // 使用 class属性获取的类对象 [self getMehtodsOfClass:[self.person1 class]];

输出:

2018-11-02 15:45:07.918209+0800 KVO原理[4369:190437] NSKVONotifying_XGPerson对象-所有方法:| setAge:| class| dealloc| _isKVOA
2018-11-02 15:45:07.918371+0800 KVO原理[4369:190437] XGPerson对象-所有方法:| .cxx_destruct| name| willChangeValueForKey:| didChangeValueForKey:| setName:| setAge:| age

通过结果可以看出,这个子类里面就是重写了3个父类方法,还有一个私有的方法,我们XGPerson这个类还有一个name属性,这里为什么没有setName呢?因为我们没有给 name 属性添加KVO,所以就不会重写它,这里面确实有那个 class 方法,确实被重写了,所以当我们使用 [self.person1 class] 的方式的时候它内部怎么返回的就清楚了。

NSKVONotifying_XGPerson 伪代码实现

通过上面的研究,我们大概也能清楚NSKVONotifying_XGPerson这个子类里面是如何实现的了,大概的代码如下:

头文件:

@interface NSKVONotifying_XGPerson : XGPerson

@end

实现:

#import "NSKVONotifying_XGPerson.h"

// KVO的原理伪代码实现
@implementation NSKVONotifying_XGPerson

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

- (void)_NSSetIntValueAndNotify{
    
    // KVO的调用顺序
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    // KVO会在didChangeValueForKey里面调用age属性变更的通知回调
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key{
// 通知监听器,某某属性值发生了改变 [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil]; } // 会重写class返回父类的class // 原因:1.为了隐藏这个动态的子类 2.为了让开发者不那么迷惑 - (Class)class{ return [XGPerson class]; } - (void)dealloc{ // 回收工作 } - (BOOL)_isKVOA{ return YES; }

 

如何手动调用KVO

其实通过上面的代码大家已经知道了KVO是怎么触发的了,那怎么手动调用呢?很简单,只要调用两个方法就行了,如下:

 

    [self.person1 willChangeValueForKey:@"age"];
    [self.person1 didChangeValueForKey:@"age"];

但是上面说调用顺序的时候,好像明明KVO是在 didChangeVlaueForKey 里面调用的,为什么还要调用 willChangeVlaueForKey呢?

那是因为KVO调用的时候会去判断这个对象有没有调用 willChangeVlaueForKey 只有调用了这个之后,再调用 didChangeVlaueForKey 才能真正触发KVO

直接修改成员变量会触发KVO吗?

答案是不会的,为什么呢?因为KVO是通过修改set方法实现来触发的,一个成员变量都没有 set 方法,所以肯定是不会触发了.

总结

KVO是通过runtime机制动态的给要添加KVO监听的对象创建一个子类,并且让instance对象的isa指向这个全新的子类.

当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数,顺序如下:

  • willChangeValueForKey:
  • 父类原来的setter
  • didChangeValueForKey:

didChangeValueForKey 内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)

通过这个子类重写一些父类的方法达到触发KVO回调的目的.

 

补充

KVO是使用了典型的发布订阅者设计模式实现事件回调的功能,多个订阅者,一个发布者,简单的实现如下:

1> 订阅者向发布者进行订阅.

2> 发布者将订阅者信息保存到一个集合中.

3> 当触发事件后,发布者就遍历这个集合分别调用之前的订阅者,从而达到1对多的通知.

 

以上已全部完毕,如有什么不正确的地方大家可以指出~~ ^_^ 下次再见~~

  

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