欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

【OC底层】KVO原理

程序员文章站 2022-09-11 17:27:16
KVO的原理是什么?底层是如何实现的? 我们可以通过代码去探索一下。 创建自定义类:XGPerson 我们的思路就是看看对象添加KVO之前和之后有什么变化,是否有区别,代码如下: 输出: 从上面可以看出,object_getClass 和 class 方式分别获取到的 类对象竟然不一样,在对象添加了 ......

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:(nsdictionary<nskeyvaluechangekey,id> *)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] 监听到<xgperson: 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是通过runtime机制动态的给要添加kvo监听的对象创建一个子类,并且让instance对象的isa指向这个全新的子类.

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

  • willchangevalueforkey:
  • 父类原来的setter
  • didchangevalueforkey:

didchangevalueforkey 内部会触发监听器(oberser)的监听方法( observevalueforkeypath:ofobject:change:context:)

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

 

补充

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

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

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

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

 

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