文章目录

  • 1. 简介
  • 2. 基本使用
    • 2.1 设置观察者
    • 2.2 接收属性改变消息
    • 2.3 移除观察者
    • 2.4 KVO 使用实例
  • 3. 原理剖析
    • 3.1 KVO 的实现
    • 3.2 NSKVONotifying_XXX类探究
    • 3.3 NSKVONotifying_XXX 中 setter 方法的实现
  • 4. 面试题解析
    • 4.1 KVO的是如何实现的?
    • 4.2 如何手动触发KVO?
    • 4.3 直接改变成员变量会触发KVO吗?
  • 参考资料

1. 简介

KVO 是 Key Value Observe 的缩写,主要通过为需要监听的对象属性设置观察者,让观察者接收到属性值改变的消息通知,是 iOS 对观察者模式的一种实现。

2. 基本使用

2.1 设置观察者

当我们需要这个为某个对象设置观察者时,可以使用一下方法:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPathoptions:(NSKeyValueObservingOptions)options context:(nullable void *)context;

方法中各参数的含义:

  • observer:观察者对象

  • keyPath:需要观察的属性在对象中的路径,相关解释如下:

    // 汽车类
    @interface Car : NSObject
    @property (nonatomic, copy) NSString *brandName; // 汽车品牌名称
    @end// Person 类
    @interface Person : NSObject
    @property (nonatomic, assign) NSInteger age; // 年龄
    @property (nonatomic, strong) Car *car;      // 持有的汽车对象
    @endPerson *p = [Person new];
    // 如果要监听这个人的年龄,keyPath 为 @"age"
    // 如果要监听这个人的汽车的品牌名称,keyPath 为 @"car.brandName"
    
  • options:监听选项,用于设置属性改变后需要接收的值。该枚举类型的定义如下:

    typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {// 提供属性改变后的新值NSKeyValueObservingOptionNew = 0x01,// 提供属性改变前的旧值  NSKeyValueObservingOptionOld = 0x02,// 如果指定,则在添加观察者的时候(注册方法返回前)立即发送一个通知给观察者,NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,// 如果指定,则在每次修改属性时,会在修改通知被发送之前预先发送一条通知给观察者,与-willChangeValueForKey:被触发的时间是相对应的。NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
    };
    
  • context:上下文信息,会随着监听到的信息一起传给观察者,可以设置为nil

2.2 接收属性改变消息

如果某个类成为了观察者,就需要重写以下方法。当监听的属性值被改变时,方法就会被调用。

/// @param keyPath 发生改变的属性路径
/// @param object 对应的观察者对象
/// @param change 可以根据监听选项获取到相应值的字典
/// @param context 在注册观察者时传入的上下文信息
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

2.3 移除观察者

我们可以通过两种以下方法移除观察者。根据 Foundation 框架中的注释表述,通常我们只要使用方法 2 就可以了。

但是当我们对同一个对象的同一属性多次添加同一个观察者,但传入不同的上下文信息(context)时,我们就必须要使用方法 1 进行移除,同时传入的上下文信息(context)要与添加时相对应。

// 1
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));// 2
- (void)removeObserver:(NSObject *)observerforKeyPath:(NSString *)keyPath;

注意:addObserver:removeObserver: 必须成对出现,否则会在观察者被释放后,因为收到 KVO 监听的消息导致 Crash。官方建议在 init 方法中添加观察者,dealloc方法中移除观察者,以保证它们可以成对出现。

2.4 KVO 使用实例

接下来我们以一个简单的实例来演示 KVO 的使用。首先创建一个只包含 age 属性的 Person,用来作为被监听的对象。

/// Person 类
@interface Person : NSObject
@property(nonatomic, assign) NSInteger age; // 年龄属性
@end

在本例中,我们给 person 对象的 age 属性添加一个监听器,当用户点击屏幕时,会更改 person 对象的 age 属性的值,进行触发 KVO。

@interface ViewController ()
@property (nonatomic, strong) Person *person; // person对象
@end@implementation ViewController- (void)dealloc {// 移除观察者[self.person removeObserver:self forKeyPath:@"age"];
}- (void)viewDidLoad {[super viewDidLoad];// 初始化,并设置 age 的初始值self.person = [[Person alloc] init];self.person.age = 18;// 监听选项,这里选择“原来的值”和“改变后的值”NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;// 为属性 person 添加观察者[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
}/// 屏幕的触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {// 改变 age 的值self.person.age = 19;
}- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@"keyPath: %@,\n object: %@,\n change: %@,\n context: %@", keyPath, object, change, context);
}@end

在点击屏幕之后,我们可以看到控制台打印出了以下信息:

可以看到,我们可以根据监听选项获取到该属性改变前和改变后的值。通常我们会根据 keyPath 和 object 来区分对不同属性的监听,然后改变的值来进行相应操作。

3. 原理剖析

学习一样知识,不能总停留在使用的表层,应该深入去学习它内部的实现原理,下面我们就一步步学习 KVO 的实现原理。

3.1 KVO 的实现

我们利用 runtime 提供的 object_getClass() 函数去获取对象在添加 KVO 监听前和添加 KVO 监听后,isa 指针所指向的类对象。

NSLog(@"person 添加KVO监听之前 - %@", object_getClass(self.person));// 监听选项,这里选择“原来的值”和“改变后的值”
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
// 为属性 person 添加观察者
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];NSLog(@"person 添加KVO监听之后 - %@", object_getClass(self.person));

程序运行后可以看到控制台打印的结果:

可以发现,在添加 KVO 监听前,person 对象的 isa 指针是指向 Person 类对象的;而在添加了 KVO 监听后,person 对象的 isa 指针是指向了一个叫 NSKVONotifying_Person 的类对象。

这说明,KVO 的实现是通过 runtime API 生成一个叫 NSKVONotifying_XXX 的类,然后将被监听对象的 isa 指针指向该类对象,以此为基础来实现的。

3.2 NSKVONotifying_XXX类探究

为了能够探究 NSKVONotifying_XXX 的作用,我们可以利用下面的方法将它内部的方法列表打印出来。

// 打印类中所有的方法
- (void)printMethodNamesOfClass:(Class)cls {unsigned int outCount;// 获取方法数组Method *methodList = class_copyMethodList(cls, &outCount);// 存储方法名NSMutableString *methodNames = [NSMutableString string];// 遍历所有方法for (int i = 0; i < outCount; i++) {// 获取方法Method method = methodList[i];// 获取方法名NSString *methodName = NSStringFromSelector(method_getName(method));[methodNames appendString:methodName];if (i != outCount - 1) {[methodNames appendString:@", "];}}free(methodList); // 需要手动释放方案列表内存NSLog(@"%@", methodNames);
}

在添加了 KVO 之后,我们通过下面代码,打印 NSKVONotifying_XXX 中包含的方法。

[self printMethodNamesOfClass:object_getClass(self.person)];

控制台的打印结果如下,可以看到其中包含 setAge:, class, dealloc_isKVOA 四个方法。

从我们的开发经验中,我们不难猜测, KVO 是通过在生成的 NSKVONotifying_XXX 类中重写 setter 方法来实现的

而对与其他三个方法,这里也给出基于经验猜测的参考解释:

  • class为了对外屏蔽生成 NSKVONotifying_XXX 类,使用者还是认为自己正在使用的是原来的类

    我们在添加 KVO 之后,利用 class 方法获取 person 的类,可以发现返回的还是 Person,这也侧面验证了上述的想法。

    NSLog(@"person 添加KVO监听之后 - %@", [self.person class]);
    
  • dealloc 是用于在对象销毁前进行收尾工作。

  • _isKVO 是为了判断该类是否为了 KVO 而生成的。

3.3 NSKVONotifying_XXX 中 setter 方法的实现

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

4. 面试题解析

4.1 KVO的是如何实现的?

通过 runtime API 生成 NSKVONotifying_XXX 类,在其中重写所监听属性的 setter 方法,然后将被监听对象的 isa 指针该类对象。

于是,当被监听属性的值通过 setter 方法被修改之后,就会触发 KVO。

4.2 如何手动触发KVO?

我们可以通过调用 willChangeValueForKey:didChangeValueForKey: 来触发 KVO。假如我们在 3.1 的例子中要手动触发 KVO,可以使用下面的代码。

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

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

@interface Person : NSObject {@publicNSInteger _age; // 成员变量
}
@property(nonatomic, assign) NSInteger age; // 属性
@end

我们直接对成员变量 _age 进行修改,是不会触发 KVO的。因为 KVO 的触发,需要调用到生成 NSKVONotifying_Person 类中的 setter 方法,直接修改成员变量是不会经过 setter 方法的,所以不会触发 KVO。

参考资料

  • https://www.cnblogs.com/496668219long/p/4470923.html
  • https://www.jianshu.com/p/badf5cac0130

看完要记得点赞哦,如果有问题可以提出来,大家交流一下~

KVO 从基本使用到原理剖析相关推荐

  1. socket之send和recv原理剖析

    socket之send和recv原理剖析 1. 认识TCP socket的发送和接收缓冲区 当创建一个TCP socket对象的时候会有一个发送缓冲区和一个接收缓冲区,这个发送和接收缓冲区指的就是内存 ...

  2. fastText的原理剖析

    fastText的原理剖析 1. fastText的模型架构 fastText的架构非常简单,有三层:输入层.隐含层.输出层(Hierarchical Softmax) 输入层:是对文档embeddi ...

  3. lua游戏脚本实例源码_Lua与其他宿主语言交互原理剖析

    Lua与其他宿主语言交互原理剖析 题外话:今天周末,刚好在家有时间就把我这次项目组内部分享的文章贴出来,分享给大家,同时也方便以后自己翻阅. 一. Lua简介 目标:Lua语言本身是用C语言来编写开发 ...

  4. Go语言底层原理剖析

    作者:郑建勋 出版社:电子工业出版社 品牌:博文视点 出版时间:2021-08-01 Go语言底层原理剖析

  5. 彻底搞透视觉三维重建:原理剖析、代码讲解、及优化改进

    视觉三维重建 = 定位定姿 + 稠密重建 + surface reconstruction +纹理贴图.三维重建技术是计算机视觉的重要技术之一,基于视觉的三维重建技术通过深度数据获取.预处理.点云配准 ...

  6. Elasticsearch分布式一致性原理剖析(一)-节点篇

    2019独角兽企业重金招聘Python工程师标准>>> 摘要: ES目前是最流行的开源分布式搜索引擎系统,其使用Lucene作为单机存储引擎并提供强大的搜索查询能力.学习其搜索原理, ...

  7. java 反序列化 ysoserial exploit/JRMPListener 原理剖析

    目录 0 前言 1 payloads/JRMPClient 1.1 Externalizable 1.2 生成payload 1.3 gadget链分析 2 exploit/JRMPListener ...

  8. 统计学习方法|支持向量机(SVM)原理剖析及实现

    欢迎直接到我的博客查看最近文章:www.pkudodo.com.更新会比较快,评论回复我也能比较快看见,排版也会更好一点. 原始blog链接: http://www.pkudodo.com/2018/ ...

  9. 统计学习方法|逻辑斯蒂原理剖析及实现

    欢迎直接到我的博客查看最近文章:www.pkudodo.com.更新会比较快,评论回复我也能比较快看见,排版也会更好一点. 原始blog链接: http://www.pkudodo.com/2018/ ...

最新文章

  1. 锁定计算机的事件日志,关闭并重新启动计算机后意外地在系统事件日志中记录了事件 ID 6008...
  2. server2008r2/2012R2遠程桌面-企业协议号
  3. HDU - 1528 Card Game Cheater(二分图最大匹配)
  4. MongoDB最新4.2.7版本三分片集群修改IP实操演练
  5. python图像下采样_[Python图像处理]十二.图像向下取样和向上取样
  6. 恭喜了!5 月逼自己学下这项技能,年薪 35 万起
  7. python的with as语句_python with (as)语句
  8. Ubuntu 20.04 无连接图标无网络问题
  9. mocano editor中使用代码比对功能
  10. 量化投资--技术篇(4) 投资组合策略
  11. markdown数学公式(MathJax)
  12. 8 NoSQL数据库有哪些?
  13. linux安装glib,glib源码安装使用方法
  14. android 日历开发教程,android 开发教程之日历项目实践(三)
  15. Django - ContentType
  16. 看懂Python爬虫框架,所见即所得一切皆有可能
  17. c#期末考试知识点_c#期末考试复习题
  18. Python:从入门到实践(上)--自学
  19. 【python标准库】色彩模式转换
  20. Qt编译报错:未找到文件main.obj

热门文章

  1. WIN32 多线程吃字母练习
  2. 常见的安全应用识别技术有哪些?
  3. C++用FindFirstFile、FindNext递归遍历硬盘的文件
  4. MySQL修改和删除触发器(DROP TRIGGER)
  5. Codeforces Beta Round #9 (Div. 2 Only)【未完结】
  6. Codeforces Round #498 (Div. 3)【完结】
  7. Acwing第 7 场周赛【未完结】
  8. 【PAT乙级】1030 完美数列 (25 分)
  9. Spring boot日志框架
  10. jQuery的Prettydate插件