【iOS】KVO底层原理

news2025/2/23 2:00:53

KVO底层原理

    • KVO概述
    • KVO常用方法
      • 注册监听器
      • 详细解释
        • 1. 系统不会增加观察者对象的引用计数
        • 2. 对象释放后观察者不会自动置空
        • 3. 需要自己持有观察者对象的强引用
      • 示例代码
        • Person 类
        • Observer 类
        • main 函数
      • 解释
      • 删除监听器
      • 监听器对象的监听回掉方法
    • KVO内部实现
      • _NSSetLongLongValueAndNotify

KVO概述

KVO是为了监听一个对象的某个属性值是否发生变化。在属性值发生变化的时候,肯定会调用其setter方法。所以KVO的本质就是监听 被监听对象有没有调用被监听属性对应的setter方法。

KVO常用方法

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

注册监听器

  1. 监听器对象为observer,被监听对象为消息的发送者即方法的调用者在回调函数中会被回传
  2. 监听的属性路径为keyPath支持点语法的嵌套
  3. 监听类型为options支持按位或来监听多个事件类型
  4. 监听上下文context主要用于在多个监听器对象监听相同keyPath时进行区分
  5. 添加监听器只会保留监听器对象的地址,不会增加引用,也不会在对象释放后置空,因此需要自己持有监听对象的强引用,该参数也会在回调函数中回传

对于第5条,防崩溃,正确使用方式如下:

这句话解释了KVO机制中的一个关键点:当你向一个对象添加观察者时,系统不会增加对观察者对象的引用计数,这意味着观察者对象可能会在被观察对象存活期间被释放。因此,你需要自己持有观察者对象的强引用,以确保观察者在观察期间不会被释放。

详细解释

1. 系统不会增加观察者对象的引用计数

当你调用addObserver:forKeyPath:options:context:方法时,系统只会记录观察者对象的地址,但不会增加它的引用计数。这意味着系统不会自动管理观察者对象的内存。

2. 对象释放后观察者不会自动置空

如果观察者对象被释放,而没有在被观察对象中移除观察者,那么当被观察对象的属性发生变化时,系统仍然会尝试通知已经被释放的观察者,导致崩溃。因此,在观察者对象被释放之前,你需要确保将它从被观察对象的观察列表中移除。

3. 需要自己持有观察者对象的强引用

为了避免观察者对象在观察期间被释放,你需要在合适的位置持有观察者对象的强引用。通常可以在控制器或管理类中将观察者对象作为一个属性来持有。

示例代码

下面是一个示例,展示如何正确持有观察者对象的强引用:

Person 类
@interface Person : NSObject

@property (nonatomic, strong) NSString *name;

@end

@implementation Person

@end
Observer 类
@interface Observer : NSObject

@property (nonatomic, strong) Person *person;

@end

@implementation Observer

- (instancetype)init {
    self = [super init];
    if (self) {
        _person = [[Person alloc] init];
        [_person addObserver:self
                  forKeyPath:@"name"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    }
    return self;
}

- (void)dealloc {
    [_person removeObserver:self forKeyPath:@"name"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"Name changed from %@ to %@",
              change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end
main 函数
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Observer *observer = [[Observer alloc] init];
        observer.person.name = @"John"; // 触发KVO
        observer.person.name = @"Doe";  // 触发KVO
    }
    return 0;
}

解释

  1. 在Observer类中持有Person对象的强引用

    @property (nonatomic, strong) Person *person;
    

    这样可以确保Person对象在Observer对象的生命周期内不会被释放。

  2. 在Observer类的init方法中添加观察者

    [_person addObserver:self
              forKeyPath:@"name"
                 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                 context:nil];
    
  3. 在Observer类的dealloc方法中移除观察者

    [_person removeObserver:self forKeyPath:@"name"];
    

删除监听器

  1. 监听器对象为observer,被监听对象为消息的发送者即方法的调用者,应与addObserver方法匹配
  2. 监听的属性路径为keyPath,应与addObserver方法的keyPath匹配
  3. 监听上下文context,应与addObserver方法的context匹配

-(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));

监听器对象的监听回掉方法

  1. keyPath即为监听的属性路径
  2. object为被监听的对象
  3. change保存被监听的值产生的变化
  4. context为监听上下文,由add方法回传
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
    change的值:
  • NSKeyValueChangeNewKey - 类型值为id,表示新的属性值
  • NSKeyValueChangeOldKey - 类型值为id,表示旧的属性值
  • NSKeyValueChangeKindKey - 表示属性变化的类型,类型为NSNumber (包含NSKeyValueChange枚举值)
    • NSKeyValueChangeSetting(设置新的值)
    • NSKeyValueChangeInsertion(插入元素,通常用于集合)
    • NSKeyValueChangeRemoval(移除元素,通常用于集合)
    • NSKeyValueChangeReplacement(替换元素,通常用于集合)
  • NSKeyValueChangeIndexesKey
    • 类型为NSIndexSet
    • 表示插入、移除或替换操作的索引。通常用于集合类型的属性,如数组。
  • NSKeyValueChangeNotificationIsPriorKey
    • 类型为NSNumber
    • 如果存在此键且值为YES,则表示这是一个更改前的通知。

综合代码示例:


- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        id newValue = change[NSKeyValueChangeNewKey];
        id oldValue = change[NSKeyValueChangeOldKey];
        NSNumber *kind = change[NSKeyValueChangeKindKey];
        NSIndexSet *indexes = change[NSKeyValueChangeIndexesKey];
        NSNumber *isPrior = change[NSKeyValueChangeNotificationIsPriorKey];

        NSLog(@"Name changed from %@ to %@", oldValue, newValue);

        switch (kind.integerValue) {
            case NSKeyValueChangeSetting:
                NSLog(@"Setting new value");
                break;
            case NSKeyValueChangeInsertion:
                NSLog(@"Inserting new element");
                break;
            case NSKeyValueChangeRemoval:
                NSLog(@"Removing element");
                break;
            case NSKeyValueChangeReplacement:
                NSLog(@"Replacing element");
                break;
        }

        if (indexes) {
            NSLog(@"Changed indexes: %@", indexes);
        }

        if (isPrior.boolValue) {
            NSLog(@"This is a prior notification");
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

KVO内部实现

未使用KVO监听的对象:
在这里插入图片描述

使用了KVO监听的对象:
在这里插入图片描述

  1. 重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容。KVO底层交换了 NSKVONotifying_Person 的 class 方法,让其返回 Person
  2. 重写setter方法:在新的类中会重写对应的set方法,是为了在set方法中增加另外两个方法的调用
  • (void)willChangeValueForKey:(NSString *)key
    (void)didChangeValueForKey:(NSString *)key

在didChangeValueForKey:方法再调用以下方法

  • (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  1. 重写dealloc方法,销毁新生成的NSKVONotifying_类。
  2. 重写_isKVOA方法,这个私有方法估计可能是用来标示该类是一个 KVO 机制声称的类。

_NSSetLongLongValueAndNotify

在添加KVO监听方法以后setAge方法变成了_NSSetLongLongValueAndNotify,所以我们可以大概猜测动态监听方法主要就是在这里面实现的

我们可以在终端使用nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep ValueAndNotify命令来查看NSSet*ValueAndNotify的类型
在这里插入图片描述

我们可以在Person类中重写willChangeValueForKey和didChangeValueForKey,来猜测一下_NSSetLongLongValueAndNotify的内部实现

- (void)setAge:(NSInteger)age{
	_age = age;
	NSLog(@"调用set方法");
}

- (void)willChangeValueForKey:(NSString *)key{
	[super willChangeValueForKey:key];
	NSLog(@"willChangeValueForKey");
}

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

	NSLog(@"didChangeValueForKey - begin");

	[super didChangeValueForKey:key];

	NSLog(@"didChangeValueForKey - end");
}

打印结果:
在这里插入图片描述

由此得出 _NSSetLongLongValueAndNotify内部实现为:

  1. 调用willChangeValueForKey方法
  2. 调用setAge方法
  3. 调用didChangeValueForKey方法
  4. didChangeValueForKey方法内部调用observer的observeValueForKeyPath:ofObject:change:context:方法

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

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

相关文章

脑网络布线成本优化——从Caja守恒原则到最小化成本的探索

脑网络布线成本优化——从Caja守恒原则到最小化成本的探索 Caja守恒原则的核心作用 Caja守恒原则&#xff0c;即大脑组织的布线成本最小化原则&#xff0c;是神经科学中的一个重要概念。它指出&#xff0c;大脑在组织结构上倾向于最小化连接神经元以构成环路或网络所涉及的布…

掌握 Python 面向对象编程与模块化导入技巧

文章目录 前言一、封装、继承、多态1. 封装2. 继承3. 多态 二、导入模块1. 导入整个模块2. 导入模块的特定部分3. 导入模块中的所有功能4. 导入整个模块并重命名5. 导入模块的特定部分并重命名 三、函数参数类型1. 位置参数2. 关键字参数3. 默认参数4. 可变参数 总结 前言 在 …

vue3-01创建项目

一、创建一个 Vue 应用 1、前提条件 前提条件&#xff1a; 已安装 18.3 或更高版本的 Node.js&#xff0c;如果当前的node 版本低于18的话&#xff0c;也没关系&#xff0c;可以使用这个命令行&#xff0c;忽略忽略引擎版本的检查 yarn config set ignore-engines true2、创建…

【数学建模】——【python】实现【最短路径】【最小生成树】【复杂网络分析】

目录 1. 最短路径问题 - 绘制城市间旅行最短路径图 题目描述&#xff1a; 要求&#xff1a; 示例数据&#xff1a; python 代码实现 实现思想&#xff1a; 要点&#xff1a; 2. 最小生成树问题 - Kruskal算法绘制MST 题目描述&#xff1a; 要求&#xff1a; 示例数据…

【前端 15】Vue生命周期

Vue生命周期 在Vue.js中&#xff0c;了解组件的生命周期对于开发者来说是至关重要的。Vue的生命周期指的是Vue实例从创建到销毁的一系列过程&#xff0c;每个阶段都对应着特定的生命周期钩子&#xff08;或称为生命周期方法&#xff09;&#xff0c;允许我们在不同的时间点加入…

【中项】系统集成项目管理工程师-第7章 软硬件系统集成-7.2基础设施集成

前言&#xff1a;系统集成项目管理工程师专业&#xff0c;现分享一些教材知识点。觉得文章还不错的喜欢点赞收藏的同时帮忙点点关注。 软考同样是国家人社部和工信部组织的国家级考试&#xff0c;全称为“全国计算机与软件专业技术资格&#xff08;水平&#xff09;考试”&…

C#实现深度优先搜索(Depth-First Search,DFS)算法

深度优先搜索&#xff08;DFS&#xff09;是一种图搜索算法&#xff0c;它尽可能深入一个分支&#xff0c;然后回溯并探索其他分支。以下是使用C#实现DFS的代码示例&#xff1a; using System; using System.Collections.Generic;class Graph {private int V; // 顶点的数量pr…

牛客算法题解:数字统计、两个数组的交集、点击消除

目录 BC153 [NOIP2010]数字统计 ▐ 题解 NC313 两个数组的交集 ▐ 题解 AB5 点击消除 ▐ 题解 BC153 [NOIP2010]数字统计 题目描述&#xff1a; 题目链接&#xff1a; [NOIP2010]数字统计_牛客题霸_牛客网 (nowcoder.com) ▐ 题解 题目要求统计出某段数组中一共有多少个…

YOLOv8不同位置引入RepVGG重参数化

一、原理解析&#xff1a; 复杂的卷积网络大都具有如下缺点&#xff1a; 复杂的多分支设计&#xff08;如ResNet中的残差相加和Inception中的分支连接&#xff09;使模型难以实现和自定义&#xff0c;降低了推理速度和降低了内存利用率。一些组件&#xff08;例如Xception和Mo…

嵌入式linux系统中压力测试的方法

在Linux环境下,确保系统各项资源充分且稳定地运行对任何系统管理员来说都至关重要。特别是在生产环境中,理解如何对系统资源进行基准测试和压力测试可以帮助预防未来的问题,同时也能够优化现有系统的性能。 在本文中,我们将探讨如何使用命令行工具来对Linux系统的CPU、内存…

开发环境搭建——Node.js

在启动前端项目的时候我们通常会用到Node.js&#xff0c;下面是对Node.js的下载安装以及配置的讲解 一、Node.js的安装 1.1、通过Node.js官网下载&#xff1a;Node.js — Run JavaScript Everywhere 下载后双击.msi安装文件后一直点击下一步即可 1.2、配置node 1.2.1、查看…

MT2140 供水管线(最小生成树Kruskal)

思路&#xff1a;Kruskal模板题 代码&#xff1a; #include<iostream> #include<stdio.h> #include<string.h> #include<vector> #include<algorithm> using namespace std;#define LL long long intconst int MAXN1e22;struct edge{int u,v,w;…

try-catch-finally 捕获异常不在catch里抛出;循环遍历对象生成任务,捕获异常对象不抛出,不影响其他正常对象生成任务

场景&#xff1a;一个模板绑定多个对象&#xff0c;要对每个对象生成任务。捕获生成任务过程中的异常&#xff0c;但是不抛出&#xff0c;只是用日志记录。这样做目的&#xff1a;循环遍历对象生成任务时&#xff0c;异常对象数据生成任务时发生异常只是导致自己生成任务失败&a…

29 列表元素访问

创建列表之后&#xff0c;可以使用整数作为下标来访问其中的元素&#xff1b;列表还支持使用负整数作为下标。 x list(hello world) print(x) print(x[0]) # 下标为0的元素&#xff0c;第一个元素 print(x[-1]) # 下标为-1的元素&#xff0c;最后一个元素x[5] print(x)

RedHat9 | Ansible 角色

环境版本说明 RedHat9 [Red Hat Enterprise Linux release 9.0]Ansible [core 2.13.3]Python [3.9.10]jinja [3.1.2] 描述角色结构 Playbook可能比较冗长且负载&#xff0c;也可能存在大量的重复代码。而角色&#xff08;roles&#xff09;可以用于层次性结构化的组织playbo…

【python】python生活管理费系统(源码+论文)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

Python爬虫入门01:在Chrome浏览器轻松抓包

文章目录 爬虫基本概念爬虫定义爬虫工作原理爬虫流程爬虫类型爬虫面临的挑战 使用Chrome浏览器抓包查看网页HTML代码查看HTTP请求请求头&#xff08;Request Header&#xff09;服务器响应抓包的意义 爬虫基本概念 爬虫定义 爬虫&#xff08;Web Crawler 或 Spider&#xff0…

【JavaEE初阶】线程安全(重点)

目录 &#x1f4d5; 线程安全的概念 &#x1f384; 观察线程不安全 &#x1f333; 线程不安全的原因 &#x1f6a9; 原因&#xff1a; &#x1f332;解决之前的线程不安全问题 &#x1f6a9; synchronized 关键字 &#x1f4d5; 线程安全的概念 如果多线程环境下…

前端面经1

1、js是单线程还是多线程&#xff1f; 单线程执行。一次只能执行一个任务&#xff0c;处理任务的方式是通过一个任务队列&#xff08;也称为消息队列&#xff09;来实现的。如果某个操作&#xff08;如网络请求或定时器&#xff09;需要花费较长时间才能完成&#xff0c;它不会…