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

KVO是Key-value observing的缩写。

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

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

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

创建自定义类:XGPerson

  1. @interface XGPerson : NSObject
  2.  
  3. @property (nonatomic,assign) int age;
  4.  
  5. @property (nonatomic,copy) NSString* name;
  6.  
  7. @end

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

  1. @interface ViewController ()
  2.  
  3. @property (strong, nonatomic) XGPerson *person1;
  4. @property (strong, nonatomic) XGPerson *person2;
  5.  
  6. @end
  7.  
  8. - (void)viewDidLoad {
  9. [super viewDidLoad];
  10.  
  11. self.person1 = [[XGPerson alloc]init];
  12. self.person2 = [[XGPerson alloc]init];
  13. self.person1.age = ;
  14. self.person2.age = ;
  15.  
  16. // 添加监听之前,获取类对象,通过两种方式分别获取 p1 和 p2的类对象
  17. NSLog(@"before getClass--->> p1:%@ p2:%@",object_getClass(self.person1),object_getClass(self.person2));
  18. NSLog(@"before class--->> p1:%@ p2:%@",[self.person1 class],[self.person2 class]);
  19.  
  20. // 添加KVO监听
  21. NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
  22. [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];
  23.  
  24. // 添加监听之后,获取类对象
  25. NSLog(@"after getClass--->> p1:%@ p2:%@",object_getClass(self.person1),object_getClass(self.person2));
  26. NSLog(@"after class--->> p1:%@ p2:%@",[self.person1 class],[self.person2 class]);
  27. }

输出:

  1. -- ::13.276167+ KVO原理[:] before getClass--->> p1:XGPerson p2:XGPerson
  2. -- ::13.276271+ KVO原理[:] before class--->> p1:XGPerson p2:XGPerson
  3.  
  4. -- ::13.276712+ KVO原理[:] after getClass--->> p1:NSKVONotifying_XGPerson p2:XGPerson
  5. -- ::13.276815+ KVO原理[:] 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之后有什么不一样.

  1. NSLog(@"person1添加KVO监听之前 - %p %p",
  2. [self.person1 methodForSelector:@selector(setAge:)],
  3. [self.person2 methodForSelector:@selector(setAge:)]);
  4.  
  5. // 添加KVO监听
  6. NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
  7. [self.person1 addObserver:self forKeyPath:@"age" options:option context:nil];
  8.  
  9. NSLog(@"person1添加KVO监听之后 - %p %p",
  10. [self.person1 methodForSelector:@selector(setAge:)],
  11. [self.person2 methodForSelector:@selector(setAge:)]);

输出:

  1. -- ::13.276402+ KVO原理[:] person1添加KVO监听之前 - 0x10277c3e0 0x10277c3e0
  2.  
  3. -- ::17.031319+ KVO原理[:] 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底层的调用顺序

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

类实现:

  1. #import "XGPerson.h"
  2.  
  3. @implementation XGPerson
  4.  
  5. - (void)setAge:(int)age{
  6.  
  7. _age = age;
  8. NSLog(@"XGPerson setAge");
  9. }
  10.  
  11. - (void)willChangeValueForKey:(NSString *)key{
  12.  
  13. [super willChangeValueForKey:key];
  14. NSLog(@"willChangeValueForKey");
  15. }
  16.  
  17. - (void)didChangeValueForKey:(NSString *)key{
  18.  
  19. NSLog(@"didChangeValueForKey - begin");
  20. [super didChangeValueForKey:key];
  21. NSLog(@"didChangeValueForKey - end");
  22. }

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

我们先设置KVO的监听回调

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

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

  1. -- ::24.788395+ KVO原理[:] willChangeValueForKey
  2. -- ::24.788573+ KVO原理[:] XGPerson setAge
  3. -- ::24.788696+ KVO原理[:] didChangeValueForKey - begin
  4. -- ::24.788893+ KVO原理[:] 监听到<XGPerson: 0x60400022f420>的age属性值改变了 -
  5. -- ::24.789014+ KVO原理[:] didChangeValueForKey - end

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

NSKVONotifying_XGPerson子类的研究

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

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

  1. // 获取一个对象的所有方法
  2. - (void)getMehtodsOfClass:(Class)cls{
  3.  
  4. unsigned int count;
  5. Method* methods = class_copyMethodList(cls, &count);
  6.  
  7. NSMutableString* methodList = [[NSMutableString alloc]init];
  8. for (int i=; i < count; i++) {
  9. Method method = methods[i];
  10. NSString* methodName = NSStringFromSelector(method_getName(method));
  11. [methodList appendString:[NSString stringWithFormat:@"| %@",methodName]];
  12. }
  13. NSLog(@"%@对象-所有方法:%@",cls,methodList);

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

}

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

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

输出:

  1. -- ::07.918209+ KVO原理[:] NSKVONotifying_XGPerson对象-所有方法:| setAge:| class| dealloc| _isKVOA
  2. -- ::07.918371+ KVO原理[:] XGPerson对象-所有方法:| .cxx_destruct| name| willChangeValueForKey:| didChangeValueForKey:| setName:| setAge:| age

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

NSKVONotifying_XGPerson 伪代码实现

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

头文件:

  1. @interface NSKVONotifying_XGPerson : XGPerson
  2.  
  3. @end

实现:

  1. #import "NSKVONotifying_XGPerson.h"
  2.  
  3. // KVO的原理伪代码实现
  4. @implementation NSKVONotifying_XGPerson
  5.  
  6. - (void)setAge:(int)age{
  7.  
  8. _NSSetIntValueAndNotify();
  9. }
  10.  
  11. - (void)_NSSetIntValueAndNotify{
  12.  
  13. // KVO的调用顺序
  14. [self willChangeValueForKey:@"age"];
  15. [super setAge:age];
  16. // KVO会在didChangeValueForKey里面调用age属性变更的通知回调
  17. [self didChangeValueForKey:@"age"];
  18. }
  19.  
  20. - (void)didChangeValueForKey:(NSString *)key{
  21. // 通知监听器,某某属性值发生了改变
  22. [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
  23. }
  24.  
  25. // 会重写class返回父类的class
  26. // 原因:1.为了隐藏这个动态的子类 2.为了让开发者不那么迷惑
  27. - (Class)class{
  28.  
  29. return [XGPerson class];
  30. }
  31.  
  32. - (void)dealloc{
  33.  
  34. // 回收工作
  35. }
  36.  
  37. - (BOOL)_isKVOA{
  38.  
  39. return YES;
  40. }

如何手动调用KVO

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

  1. [self.person1 willChangeValueForKey:@"age"];
  2. [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原理的更多相关文章

  1. iOS weak底层实现原理

    今年年底做了很多决定,离开工作三年的深圳,来到了上海,发现深圳和上海在苹果这方面还是差距有点大的,上海的市场8成使用swift编程,而深圳8成的使用OC,这点还是比较让准备来上海打拼的苹果工程师有点小 ...

  2. KVO原理解析

    KVO在我们项目开发中,经常被用到,但很少会被人关注,但如果面试一些大公司,针对KVO的面试题可能如下: 知道KVO嘛,底层是怎么实现的? 如何动态的生成一个类? 今天我们围绕上面几个问题,我们先看K ...

  3. PHP底层工作原理

    最近搭建服务器,突然感觉lamp之间到底是怎么工作的,或者是怎么联系起来?平时只是写程序,重来没有思考过他们之间的工作原理: PHP底层工作原理 图1 php结构 从图上可以看出,php从下到上是一个 ...

  4. Java并发之底层实现原理学习笔记

    本篇博文将介绍java并发底层的实现原理,我们知道java实现的并发操作最后肯定是由我们的CPU完成的,中间经历了将java源码编译成.class文件,然后进行加载,然后虚拟机执行引擎进行执行,解释为 ...

  5. spirng底层实现原理

    什么是框架?框架解决的是什么问题? 编程有一个准则,Don't Repeat Yourself(不要重复你的代码),所以我们会将重复的代码抽取出来,封装到方法中:如果封装的方法过多,将将这些方法封装成 ...

  6. 《Java并发编程的艺术》Java并发机制的底层实现原理(二)

    Java并发机制的底层实现原理 1.volatile volatile相当于轻量级的synchronized,在并发编程中保证数据的可见性,使用 valotile 修饰的变量,其内存模型会增加一个 L ...

  7. Spring(二)IOC底层实现原理

    IOC原理 将对象创建交给Spring去管理. 实现IOC的两种方式 IOC配置文件的方式 IOC注解的方式 IOC底层实现原理 底层实现使用的技术 1.1 xml配置文件 1.2 dom4j解析xm ...

  8. iOS分类底层实现原理小记

    摘要:iOS分类底层是怎么实现的?本文将分如下四个模块进行探究分类的结构体编译时的分类分类的加载总结本文使用的runtime源码版本是objc4-680文中类与分类代码如下//类@interfaceP ...

  9. java并发编程系列七:volatile和sinchronized底层实现原理

    一.线程安全 1.  怎样让多线程下的类安全起来 无状态.加锁.让类不可变.栈封闭.安全的发布对象 2. 死锁 2.1 死锁概念及解决死锁的原则 一定发生在多个线程争夺多个资源里的情况下,发生的原因是 ...

随机推荐

  1. 事件驱动模型 IO多路复用 阻塞IO与非阻塞IO select epool

    一.事件驱动 1.要理解事件驱动和程序,就需要与非事件驱动的程序进行比较.实际上,现代的程序大多是事件驱动的,比如多线程的程序,肯定是事件驱动的.早期则存在许多非事件驱动的程序,这样的程序,在需要等待 ...

  2. wdcpV3面板安装ssl证书 apache教程 子站SSL配置

    本帖最后由 q1082121 于 2016-11-24 12:31 编辑 方案二 apache1.把apache类型的ssl三个文件上传到:/www/wdlinux//www/wdlinux/http ...

  3. 安装nvm之后node不可用,“node”不是内部或外部命令,也不是可运行的程序或批处理文件(ng)

    安装nvm: 1.下载nvm压缩包地址:https://github.com/coreybutler/nvm-windows/releases 2.下载后解压在目标文件夹中,我这里是H:\applic ...

  4. 利用 NGINX 最大化 Python 性能,第二部分:负载均衡和监控

    [编者按]本文主要介绍 NGINX 的主要功能以及如何通过 Nginx 优化 Python 应用性能.本文系国内 ITOM 管理平台 OneAPM 编译呈现. 本文上一篇系: 利用 NGINX 最大化 ...

  5. seo关键词

    除非你站有很高的权重. 小道消息称keywords曾被百度.谷歌.雅虎等搜索引擎剔除,将不会再影响搜索引擎的排序结果,小编认为设置一下总没坏处,还是有一些搜索引擎比较重视keywords标签的. 用法 ...

  6. 1.创建maven 项目 动态web工程完整示例

    注意,以下所有需要建立在你的eclipse等已经集成配置好了maven了,说白了就是新建项目的时候已经可以找到maven了 没有的话需要安装maven 一.创建项目 1.新建maven项目,如果不在上 ...

  7. MVC技术的面试问题

    MVC中的三种方式: ORM框架:对象关系映射关系 ,面向对象的对象模型和关系型数据之间的相互转换.基于关系型数据库的数据存储,实现一个虚拟的面向对象的数据访问接口.只要提供了持久化类与表的映射关系, ...

  8. ARC下的block导致的循环引用问题解析

    ARC下的block导致的循环引用问题解析 更详细细节请参考 http://blog.sina.com.cn/s/blog_8c87ba3b0101m599.html ARC下,copy到堆上的blo ...

  9. UIButton的resizableImageWithCapInsets使用解析

    UIButton的resizableImageWithCapInsets使用解析 效果: 使用的源文件: 源码: // // ViewController.m // SpecialButton // ...

  10. 基于springMVC的RESTful服务实现

    一,什么是RESTful RESTful(RESTful Web Services)一种架构风格,表述性状态转移,它不是一个软件,也不是一个标准,而是一种思想,不依赖于任何通信协议,但是开发时要成功映 ...