【OC底层】KVO原理
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对多的通知.
以上已全部完毕,如有什么不正确的地方大家可以指出~~ ^_^ 下次再见~~
上一篇: 在Linux服务器上安装Linux-Dash的教程
下一篇: 自媒体平台百家号该如何去运营?