「iOS」——KVO

源码学习

  • iOS底层学习:KVO 底层原理
    • KVO
      • 注册 KVO 监听
      • 实现 KVO 监听
      • 移除 KVO 监听
      • 处理变更通知
      • 手动KVO(禁用KVO)
        • 关闭自动通知
        • 手动实现 setter 方法
      • KVO 和线程
        • 如果 KVO 是多线程的
        • **单线程的保证**
        • 如果没有 prior 选项
        • **prior 选项的作用**
    • KVO 实现原理
      • 派生类重写的方法
      • 验证 isa 指向示例
      • KVO 注意事项
      • 问题总结


iOS底层学习:KVO 底层原理

KVO

KVO 的全称是 KeyValueObserving,俗称 “键值监听 ",可以用于监听某个对象属性值的改变;KVO 可以通过监听 key,来获得 value 的变化,用来在对象之间监听状态变化。

基本思想:对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的 KVO 接口方法,来自动的通知观察者。

KVO 是苹果提供的一套事件通知机制。KVO 和 NSNotificationCenter 都是 iOS 中观察者模式的一种实现,区别是:NSNotificationCenter 可以是一对多的关系,而 KVO 是一对一的;

注册 KVO 监听

通过[addObserver:forKeyPath:options:context:]方法注册 KVO,这样可以接收到 keyPath 属性的变化事件:

  • observer:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context:方法。

  • keyPath:要观察的属性名称,需与属性声明的名称一致。

  • options:回调方法中收到被观察者的属性的旧值或新值等,对 KVO 机制进行配置,修改 KVO 通知的时机以及通知的内容。

  • context:上下文,会传递到观察者的函数中,用于区分消息,应当为不同值。

options所包括的内容:

  • NSKeyValueObservingOptionNew:change 字典包括改变后的值。

  • NSKeyValueObservingOptionOld:change 字典包括改变前的值。

  • NSKeyValueObservingOptionInitial:注册后立刻触发 KVO 通知。

  • NSKeyValueObservingOptionPrior:值改变前是否也要通知(决定是否在改变前、改变后通知两次)。

实现 KVO 监听

通过方法[observeValueForKeyPath:ofObject:change:context:]实现 KVO 的监听:

- (void)observeValueForKeyPath:(NSString *)keyPath  ofObject:(id)object  change:(NSDictionary *)change  context:(void *)context  
  • keyPath:被观察对象的属性。

  • object:被观察的对象。

  • change:字典,存放相关的值,根据options传入的枚举返回新值、旧值。

  • context:注册观察者时传递的context值。

移除 KVO 监听

通过方法[removeObserver:forKeyPath:]移除监听;

处理变更通知

每当监听的 keyPath 发生变化时,会在observeValueForKeyPath函数中回调

- (void)observeValueForKeyPath:(NSString *)keyPathofObject:(id)objectchange:(NSDictionary *)changecontext:(void *)context

change字典保存了变更信息,具体内容取决于注册时的NSKeyValueObservingOptions

手动KVO(禁用KVO)

KVO 的实现是在注册的 keyPath 的 setter 方法中,自动插入并调用了两个函数:

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

手动实现 KVO 需先关闭自动生成 KVO 通知,再手动调用通知方法,可灵活添加判断条件。

关闭自动通知
 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {  if ([key isEqualToString:@"age"]) {  return NO;  } else {  return [super automaticallyNotifiesObserversForKey:key];  }  
}  
手动实现 setter 方法

接着手动实现属性的 setter 方法,在setter方法中先调用willChangeValueForKey:接着进行赋值操作,然后调用didChangeValueForKey:

- (void)setAge:(int)theAge {  [self willChangeValueForKey:@"age"];  age = theAge;  [self didChangeValueForKey:@"age"];  
}  

KVO 和线程

  • KVO 行为是同步的,在所观察的值发生变化的同一线程上触发,无队列或 Runloop 处理。

  • 手动或自动调用didChangeValueForKey:会触发 KVO 通知。

  • 单线程保证(如主队列):

  1. 确保所有监听某一属性的观察者在 setter 方法返回前被通知到。

  2. 若键观察时附上NSKeyValueObservingOptionPrior选项,直到observeValueForKeyPath被调用前,监听的属性返回值不变。

    1. 该键对应的值是一个 NSNumberBOOL 类型),用于判断当前 KVO 通知是在属性值 变更前(前置通知,值为 YES)还是 变更后(后置通知,值为 NO)发送。

上述两个特点可以有效解决复杂场景下的数据一致性时序问题

我们看以下代码:

// User.h
@interface User : NSObject
@property (nonatomic, assign) NSInteger age;
@end// ViewController.m
- (void)viewDidLoad {[super viewDidLoad];// 注册 KVO 监听[self.user addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];// 主线程修改 agedispatch_async(dispatch_get_main_queue(), ^{self.user.age = 20;NSLog(@"主线程修改 age 为 20");});// 子线程同时修改 agedispatch_async(dispatch_get_global_queue(0, 0), ^{self.user.age = 30;NSLog(@"子线程修改 age 为 30");});
}// KVO 回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {if ([keyPath isEqualToString:@"age"]) {NSNumber *newAge = change[NSKeyValueChangeNewKey];NSLog(@"KVO 接收到 age 变化: %@", newAge);}
}
如果 KVO 是多线程的
  • 可能出现通知丢失:主线程和子线程同时修改 age,观察者可能只收到最后一次通知(如只收到 30,丢失 20)。
  • 可能出现通知顺序错乱:观察者先收到 30 的通知,再收到 20 的通知,导致逻辑混乱。
单线程的保证
  • 原子性:KVO 会在 setter 方法返回前同步且顺序地通知所有观察者,确保:
    1. 所有观察者都能收到每一次变化。
    2. 通知顺序与 setter 调用顺序一致(先收到 20,再收到 30)。

再来学习NSKeyValueObservingOptionPrior。该属性主要应用在复杂数据更新与 UI 动画同步

// 注册 KVO,带上 prior 选项
[self.dataSource addObserver:self forKeyPath:@"items" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionPrior) context:nil];// KVO 回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {if ([keyPath isEqualToString:@"items"]) {// 1. 先收到 prior 通知(change[NSKeyValueChangeNotificationIsPriorKey] = @YES)if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {// 准备动画(此时 items 还是旧值)[self.tableView beginUpdates];} // 2. 再收到实际变化通知else {// 执行动画(此时 items 已是新值)[self.tableView endUpdates]; // 自动触发 insert/delete 动画}}
}
如果没有 prior 选项
  • 直接在 endUpdates 时才知道数据变化,无法提前准备动画。
  • 可能导致 UI 闪烁或动画不连贯。
prior 选项的作用
  • 分阶段通知
    1. 第一次通知:在属性值实际变更前触发(NSKeyValueChangeNotificationIsPriorKey = @YES),此时属性值仍为旧值。
    2. 第二次通知:在属性值变更后触发(默认行为),此时属性值已更新。
  • 实际应用
    • 在第一次通知时,计算新旧数据的差异(如哪些行需要插入 / 删除)。
    • 在第二次通知时,执行 beginUpdates/endUpdates,让表格视图平滑过渡。

KVO 实现原理

KVO 通过isa-swizzling实现,基本流程如下:

isa-swizzling 的本质:

修改对象的类型:通过修改对象的 isa 指针,使其指向另一个类,从而改变对象的行为。

  1. 创建派生类:编译器自动为被观察对象创建派生类(如NSKVONotifying_XXX),将被观察实例的isa指向该派生类,派生类的superclass指向原类。

  2. 重写方法:若注册了某属性的观察,派生类会重写该属性的 setter 方法,并添加通知代码。

  3. 消息传递:Objective-C 通过isa指针找到对象所属类,调用派生类重写后的方法,触发通知。

派生类重写的方法

  • setter 方法:插入willChangeValueForKey:didChangeValueForKey:调用,触发通知。
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

然后在 didChangeValueForKey: 中,去调用:

- (void)observeValueForKeyPath:(nullable NSString *)keyPathofObject:(nullable id)objectchange:(nullable NSDictionary<NSKeyValueChangeKey, id> *)changecontext:(nullable void *)context;
  • class 方法:返回原类,隐藏子类存在,避免isKindOfClass判断异常。
- (Class)class {  return class_getSuperclass(object_getClass(self));  
}  
  • dealloc 方法:释放 KVO 相关资源。

  • _isKVOA 方法:返回YES,标识该类为 KVO 生成的子类。

请添加图片描述

验证 isa 指向示例

#import <Foundation/Foundation.h>  
#import <objc/runtime.h>  @interface ObjectA: NSObject  
@property (nonatomic) NSInteger age;  
@end  @implementation ObjectA  
@end  @interface ObjectB: NSObject  
@end  @implementation ObjectB  
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {  NSLog(@"%@", change);  
}  
@end  int main(int argc, const char * argv[]) {  @autoreleasepool {  ObjectA *objA = [[ObjectA alloc] init];  ObjectB *objB = [[ObjectB alloc] init];  [objA addObserver:objB forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];  NSLog(@"%@", [objA class]); // 输出:ObjectA(表面类型)  NSLog(@"%@", object_getClass(objA)); // 输出:NSKVONotifying_ObjectA(实际类型)  }  return 0;  
}  
  • class方法返回对象所属的类(原类)。

  • object_getClass返回对象的isa指向的实际类(派生类)。

请添加图片描述

KVO 注意事项

  1. 内存管理
  • addObserverremoveObserver需成对调用,避免观察者释放后仍接收通知导致 Crash。

  • KVO 不对观察者强引用,需注意观察者生命周期。否则会导致观察者被释放带来的Crash。

  1. 方法实现:观察者必须实现observeValueForKeyPath:ofObject:change:context:方法,否则崩溃。

  2. KeyPath 安全:在调用KVO时需要传入一个keyPath,由于keyPath是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash。我们可以利用系统的反射机制将keyPath反射出来,这样编译器可以在@selector()中进行合法性检查。

  3. 数组监听:默认仅监听数组对象本身变化,需通过mutableArrayValueForKey操作数组或手动触发通知来监听元素变化。

问题总结

  1. **直接修改成员变量是否触发 KVO?**不会。KVO 本质是替换 setter 方法,仅通过 setter 或 KVC 修改属性值时触发。

  2. **KVC 修改属性会触发 KVO 吗?**会。setValue:forKey:会调用willChangeValueForKeydidChangeValueForKey,触发监听器回调。

  3. 如何监听数组元素变化?

  • 使用NSMutableArray并通过mutableArrayValueForKey获取数组,其操作会自动触发通知。

  • 手动调用willChangeValueForKeydidChangeValueForKey触发通知。

当数组中的元素发生变化时,手动触发KVO通知即可实现监听。具体实现方式如下:

使用NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld选项来监听可变数组中的元素变化。这两个选项会在KVO通知中包含新旧值的信息,因此可以在观察者中获取到数组中元素的变化。
代码如下:

[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
//观察者中实现.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {if ([keyPath isEqualToString:@"myArray"]) {NSArray *oldArray = change[NSKeyValueChangeOldKey];NSArray *newArray = change[NSKeyValueChangeNewKey];// 处理数组元素的变化}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/90169.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/90169.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unreal5从入门到精通之使用 Python 编写虚幻编辑器脚本

文章目录 前言 如何运行Python 1.控制台 2.蓝图调用python python 入门 变量 数据类型 运算符 条件判断 循环 函数 模块引用 类型转换 类 类方法 继承 构造函数 unreal API 创建材质 创建材质实例 获取Content下选中资源 获取关卡中选中Actors 放置Cube 编辑器进度条 展示对话框…

Django3 - Web前端开发基础 HTML、CSS和JavaScript

网站开发可以分为前端开发和后端开发&#xff0c;前端开发是指网页设计&#xff0c;我们在浏览器看到网站的图片、文字、音乐视频等内容排版都是由前端开发人员实现的&#xff1b;后端开发是为前端开发提供实际的数据内容和业务逻辑&#xff0c;比如提供文字内容、图片和音乐视…

Nginx和Apache的区别

一。Nginx和Apache的优缺点和对比Nginx 优点Apache 优点性能与并发采用事件驱动模型&#xff0c;支持 10 万 高并发连接&#xff0c;资源&#xff08;CPU / 内存&#xff09;占用极低生态成熟&#xff0c;内置模块可直接处理动态内容&#xff0c;无需依赖第三方程序配置与部署…

前端实现可编辑脑图的方案

前端实现可编辑脑图的方案 实现可编辑脑图(Mind Map)在前端有多种方案&#xff0c;以下是一些主流的技术方案&#xff1a; 1. 基于现有开源库的方案 JavaScript 库 MindElixir: 轻量级开源脑图库&#xff0c;支持节点增删改、拖拽、导入导出等 GitHub: https://github.com/sssh…

7-大语言模型—指令理解:指令微调训练+模型微调

目录 1、指令微调的训练过程 2、指令微调数据 2.1、“指令输入” 2.2、“答案输出” 3、指令微调数据的构建方法 3.1、手动构建&#xff1a;纯人工 “出题 写答案” 3.1.1、构建流程 3.1.1.1、定义任务类型 3.1.1.2、设计指令模板 3.1.1.3、人工标注响应 3.1.2、工…

服务器版本信息泄露-iis返回包暴露服务器版本信息

漏洞信息描述&#xff1a;服务器版本信息泄露 测试过程&#xff1a;访问http://192.168.23.63&#xff0c;看返回包可以得知服务器版本信息 显示暴露返回server版本信息 修复建议&#xff1a;限制返回包带有服务器版本信息 如何隐藏IIS Web服务响应头中的IIS Server版本信息…

rust嵌入式开发零基础入门教程(二)

本教程的第二部分&#xff0c;我们将深入理解 Rust 语言的核心概念——所有权&#xff08;Ownership&#xff09;、借用&#xff08;Borrowing&#xff09;和生命周期&#xff08;Lifetimes&#xff09;。这些是 Rust 内存安全的基础&#xff0c;也是初学者理解 Rust 最关键的部…

【黑产大数据】2025年上半年互联网黑灰产趋势年度总结

2025年上半年&#xff0c;互联网黑灰产攻击持续演化&#xff0c;呈现出更隐蔽、更智能、更产业化的趋势。黑灰产从业人员数量继续增长&#xff0c;攻击资源、技术与作案场景全面升级。整体来看&#xff0c;2025年上半年黑灰产行业发生的几大事件&#xff0c;也时刻印证了黑灰产…

低代码/无代码平台如何重塑开发生态

低代码/无代码平台通过降低技术门槛、提升开发效率、推动业务和IT深度融合重塑开发生态。 具体而言&#xff0c;低代码/无代码平台极大降低了应用开发的技术门槛&#xff0c;使得非专业人员也能轻松构建业务应用。此外&#xff0c;它们通过可视化的开发模式&#xff0c;大幅提升…

ICA学习(2)

1.公式推导1.1两个问题ICA算法会带来2个不确定性&#xff1a;幅值不确定性和顺序不确定性。1.2 推导观测数据 x 是盲源 s 的线性混合&#xff1a;x As (1)此时&#xff0c;W矩阵是未知的&#xff0c;ICA算法的目的便是找到一个最优的矩阵W&#xff0c;实现对矩阵…

【愚公系列】《MIoT.VC》002-构建基本仿真工作站(布局一个基本工作站)

💎【行业认证权威头衔】 ✔ 华为云天团核心成员:特约编辑/云享专家/开发者专家/产品云测专家 ✔ 开发者社区全满贯:CSDN博客&商业化双料专家/阿里云签约作者/腾讯云内容共创官/掘金&亚马逊&51CTO顶级博主 ✔ 技术生态共建先锋:横跨鸿蒙、云计算、AI等前沿领域…

网络协议相关

OSI七层模型包含物理层、数据链路层、网络层、传输层、会话层、表示层和应用层;TCP/IP四层模型将其简化为网络接口层、网络层、传输层和应用层;映射关系:例如OSI的物理层和数据链路层对应TCP/IP的网络接口层&#xff0c;主要处理MAC地址寻址和物理介质传输。协议模型对比两者的…

【CNN】LeNet网络架构

1.MLP多层感知机MLP&#xff08;Multilayer Perceptron&#xff09;&#xff0c;也是人工神经网络&#xff08;ANN&#xff0c;Artificial Neural Network&#xff09;&#xff0c;是一种全连接多层感知机&#xff08;Multilayer Perceptron, MLP&#xff09;是一种前馈神经网络…

VSCODE 禁用git 功能

第一步&#xff0c;打开设置第二步&#xff0c;搜 git:Enabled

Spring Boot05-热部署

一、Spring Boot 启动热部署Spring Boot 启动“热部署&#xff08;Hot Deployment&#xff09;”&#xff0c;可以让你在不重启项目的情况下快速看到代码变更的效果&#xff08;特别是前后端调试阶段&#xff09;。1-1、什么是热部署&#xff1f;热部署是指&#xff1a;修改 Ja…

网站域名备案和服务器有关系吗

域名备案的那些事儿域名备案&#xff0c;简单来说&#xff0c;就是把你的网站信息登记到相关管理部门那里。这就好比你开个小店&#xff0c;得去工商局登记一下&#xff0c;让人家知道你在干啥。根据我国相关规定&#xff0c;凡是使用大陆境内服务器提供服务的网站&#xff0c;…

2025华为ODB卷-推荐多样性200分-三语言题解

📌 华为OD机试真题精选 2025B卷合集 推荐多样性200分 问题描述 A先生正在设计一个推荐系统,需要考虑多样性,要求从多个列表中选择元素。系统一次性需要返回 N N N 屏数据(窗口数量),每屏展示 K K

ZeroMQ源码深度剖析:网络机制与性能优化实战

目录1 发布订阅过滤的高效实现2 ZeroMQ的核心优势3 常见Socket类型及应用4 异步连接实现机制5 断线重连机制6 高水位线&#xff08;HWM&#xff09;深度解析7 消息丢失与错误处理8 消息帧&#xff08;Frame&#xff09;高级特性9 高效性实现原理10 无锁消息队列设计11 零拷贝实…

[数据库]Neo4j图数据库搭建快速入门

[数据库]图数据库基础入门 概念 图数据库是一种使用图结构&#xff08;节点、边和属性&#xff09;进行数据存储和查询的数据库管理系统。与传统的关系型数据库不同&#xff0c;图数据库专注于实体之间的关系&#xff0c;特别适合处理高度互联的数据。常见的图数据库包括&#…

本地数据库有数据,web页面无信息显示,可能是pymysql的版本问题【pymysql连接本地数据库新旧版本的区别】

pymysql连接本地数据库新旧版本的区别新版本老版本python web下的settings文件 新版本 的pymysql 连接本地数据库&#xff1a; mysql_conn pymysql.connect(hostself.conn_infos["HOST"],userself.conn_infos["USER"],passwordself.conn_infos["PAS…