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 = ;
self.person2.age = ; // 添加监听之前,获取类对象,通过两种方式分别获取 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]);
}

输出:

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

    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:)]);

输出:

-- ::13.276402+ KVO原理[:] person1添加KVO监听之前 - 0x10277c3e0 0x10277c3e0

-- ::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底层的调用顺序

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

类实现:

#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,输出如下:

-- ::24.788395+ KVO原理[:] willChangeValueForKey
-- ::24.788573+ KVO原理[:] XGPerson setAge
-- ::24.788696+ KVO原理[:] didChangeValueForKey - begin
-- ::24.788893+ KVO原理[:] 监听到<XGPerson: 0x60400022f420>的age属性值改变了 -
-- ::24.789014+ KVO原理[:] 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=; 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]];

输出:

-- ::07.918209+ KVO原理[:] NSKVONotifying_XGPerson对象-所有方法:| setAge:| class| dealloc| _isKVOA
-- ::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这个子类里面是如何实现的了,大概的代码如下:

头文件:

@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原理的更多相关文章

  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. Scala + Thrift+ Zookeeper+Flume+Kafka配置笔记

    1. 开发环境 1.1. 软件包下载 1.1.1. JDK下载地址 http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downl ...

  2. FineReport中JS如何自定义按钮导出

    FineReport支持多种不同的导出方式,直接使用FineReport内置导出按钮可以非常快捷方便的来对各种格式的输出,但是我们在web页面集成中的时候,往往只想将报表内容嵌入到iframe中,而工 ...

  3. Android保持屏幕常亮

    Android保持屏幕常亮,PowerManager.WakeLock的使用 package com.hebaijun.wakelock;      import android.app.Activi ...

  4. frame shiro 授权及原理简述

    shiro 授权模式 shiro采用的是rbac授权模式rbac,基于角色的权限管理,谁扮演什么角色,被允许做什么事情. shiro 授权流程 shiro 授权方式 1.编程式 通过写if/else授 ...

  5. The operation names in the portType match the method names in the SEI or Web service implementation class.

    The Endpoint validation failed to validate due to the following errors: :: Invalid Endpoint Interfac ...

  6. asar 如何解密加密?electron 的 asar 的具体用法

    来源:https://newsn.net/say/electron-asar.html 在electron中,asar是个特殊的代码格式.asar包里面包含了程序猿编写的代码逻辑.默认情况下,这些代码 ...

  7. TreeSet的自然排序(自定义对象 compareTo方法)

    >要实现自然排序,对象集合必须实现Comparable接口,并重写compareTo()方法 >一般需求中描述的是"主要条件",如:按姓名长度排序.  需注意次要条件 ...

  8. python字典去重脚本

    #!/usr/bin/env python # encoding: utf-8 #字典去重小代码 import sys import os import platform try: pass exce ...

  9. 【C#】#103 动态修改App.config配置文件

    对 C/S模式 下的 App.config 配置文件的AppSetting节点,支持配置信息现改现用,并可以持久保存. 一. 先了解一下如何获取 配置信息里面的内容[获取配置信息推荐使用这个] 1.1 ...

  10. 【连接】Spring事物的传播行为

    http://blog.csdn.net/it_wangxiangpan/article/details/24180085 PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务, ...