【iOS KVO(上)实现过程】

news2024/11/24 1:23:06

前言

KVO 也适用于传值,在之前的学习只是学习了KVO的传值,今天详细学习 监听和实现

源码放在下一节学习

1.1 KVO

KVO(Key-Value Observing)是Objective-C语言中一种观察者模式的实现,可以用来监听对象属性值的变化。KVO机制允许一个对象注册为另一个对象的属性变化的观察者,并在被观察的属性值发生变化时,自动接收通知并进行相应处理。

KVO可以实现监听某个属性的变化 KVO机制只能监听对象属性值的变化,无法监听基本数据类型的变化,需要将其封装为对象属性才能实现传递。)

KVO可以实现界面之间的传值,跨界面可以。

1.2 使用KVO

现在有如下的场景
请添加图片描述
我们点击change按钮需要改变 上面的Label,同时我们需要监听Label的变化,看看如何用KVO实现键值监听

初始化按钮

- (UILabel *)test_label_init {
    if (!self.test_label) {
        self.test_label = [[UILabel alloc] init];
        self.test_label.text = @"Label not Change";
        self.test_label.font = [UIFont systemFontOfSize:25];
        [self.view addSubview:self.test_label];
        self.test_label.frame = CGRectMake(120, 170, 300, 30);
        [self.test_label addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    }
    return self.test_label;
}
- (UIButton *)test_button_init {
    if (!self.test_button) {
        self.test_button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
        [self.test_button setTitle:@"change" forState:UIControlStateNormal];
        [self.test_button addTarget:self action:@selector(changeLabel) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:self.test_button];
        self.test_button.frame = CGRectMake(120, 260, 100, 100);
        self.test_button.backgroundColor = [UIColor redColor];
    }
    return self.test_button;
}

注册KVO监听

通过[addObserver:forKeyPath:options:context:]方法注册KVO,这样可以接收到keyPath属性的变化事件;
请添加图片描述
observer:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context: 方法。
keyPath:要观察的属性名称。要和属性声明的名称一致。
options:回调方法中收到被观察者的属性的旧值或新值等,对KVO机制进行配置,修改KVO通知的时机以及通知的内容
context:传入任意类型的对象,在"接收消息回调"的代码中可以接收到这个对象,是KVO中的一种传值方式。、

KVO监听实现

通过方法[observeValueForKeyPath:ofObject:change:context:]实现KVO的监听;
请添加图片描述

keyPath:被观察对象的属性
object:被观察的对象
change:字典,存放相关的值,根据options传入的枚举来返回新值旧值
context:注册观察者的时候,context传递过来的值

点击Button

请添加图片描述
请添加图片描述

移除KVO监听

在不需要监听的时候,通过方法[removeObserver:forKeyPath:],移除监听;

禁止KVO

我们可以手动禁止KVO监听某个属性的变化

automaticallyNotifiesObserversForKey:是一个可选的类方法,用于自定义KVO机制中属性变化通知的行为。当对象的属性发生变化时,系统会自动调用这个方法来获取是否自动发送通知。

这个方法通常被用于实现一些高级的KVO机制,比如当某个属性的变化依赖于其他属性时,可以在这个方法中检测相关属性的变化情况,从而决定是否发送属性变化通知。

我们实现一个Label的分类,在里面重写这个方法

#import "UILabel+autoCall.h"

@implementation UILabel (autoCall)
// 选择性的实现KVO
// 因为是类方法 并且改变的是Label 所以需要创建一个分类来给UILabel重写这个方法 即可完成禁止通知

// 那么也就不会实现 observeValueForKeyPath
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"text"]) {
        NSLog(@"NO WAY");
        return NO;
    } else {
        return [super automaticallyNotifiesObserversForKey:key];
    }
//    return NO;
}
@end

请添加图片描述

1.3 KVO实现监听数组内部元素变化

KVO默认只能监听到数组对象本身的变化,而无法监听到数组内部元素的变化。例如,如果将一个对象添加到数组中,KVO会收到数组count属性的变化通知,但不会收到数组内部元素的变化通知。

KVO机制只能监听对象属性值的变化,无法监听基本数据类型的变化,那么如何实现KVO监听数组内部元素的变化?

KVO(Key-Value Observing)是Objective-C语言中一种观察者模式的实现,可以用来监听对象属性值的变化。但是KVO不能直接监听数组的变化,因为NSArray和NSMutableArray并没有实现KVO机制。如果需要监听数组的变化,可以使用以下两种方式:

1.3.1 手动触发KVO通知

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

在被观察对象的类中,重写该对象所包含的可变数组的对应方法,比如addObject:、removeObject:、insertObject:atIndex:等方法。
在重写的方法中,调用willChangeValueForKey:和didChangeValueForKey:方法,手动触发KVO通知。

- (void)addObject:(id)anObject {
    [self willChangeValueForKey:@"myArray"];
    [super addObject:anObject];
    [self didChangeValueForKey:@"myArray"];
}

在观察者中注册被观察对象的数组属性,当数组中的元素发生变化时,观察者会收到KVO通知。

[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

需要注意的是,手动触发KVO通知需要在重写的方法中手动添加代码,实现起来比较麻烦,容易出错,因此不是一种推荐的方式。

1.3.2 使用NSKeyValueObservingOptionNewNSKeyValueObservingOptionOld选项

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

[observedObject addObserver:self forKeyPath:@"myArray" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,根据KVO通知中的信息来处理数组元素的变化。

- (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];
        // 处理数组元素的变化
    }
}

这种方式需要被观察的对象的数组属性必须是可变的,而且只能监听到元素的增加、删除和替换操作,

1.3.4 使用KVO的注意事项⚠️

  • keyPath 不能为空字符串
  • 注意在适合的地方removeObersver,如果观察实例比被观察实例先释放,这时候改变观察属性,会产生崩溃。
  • 没有添加,直接移除观察关系,也会产生崩溃

1.4 KVO的实现

KVO的实现原理分为3步走

1.4.1 实现

首先明确 KVO机制的实现原理是,当一个对象被观察时,系统会动态地生成一个派生类并将被观察对象的isa指针指向该派生类。这个派生类重写了被观察对象的setter方法,在setter方法中,除了进行属性值的赋值操作,还会通知观察者对象属性值的变化。

如何查看派生类?
情景:现在有一个testClass的类,里面有一个className属性,现在监听className属性

  self.testClass = [[TestClass alloc] init];
    NSLog(@"%s: isa = %s", object_getClassName(self.testClass), class_getName(object_getClass(self.testClass)));
   [self.testClass addObserver:self forKeyPath:@"className" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    NSLog(@"%s: isa = %s", object_getClassName(self.testClass), class_getName(object_getClass(self.testClass)));

其中,object_getClassName()函数可以返回对象所属的类名,object_getClass()函数可以返回对象的实际类(class),而不是对象所属的类的类型。

当这段代码执行后,就会在控制台输出该对象的类名和实际类名。实际类名即为系统动态生成的派生类名,以NSKVONotifying_为前缀,后面紧跟着被观察对象的类名。例如,如果被观察对象是一个Person对象,那么实际类名就是NSKVONotifying_Person。

注意,这种方法只适用于Objective-C的KVO机制。对于Swift的KVO机制,由于其机制不同,不能通过此方法来打印查看。请添加图片描述

如何生成派生中间类?

isa-swizzling(类指针交换):
就是把当前某个实例对象的isa指针指向一个新建造的中间类,在这个新建造的中间类上面做hook方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。

请添加图片描述
请添加图片描述
在添加观察者之后 NSKVONotifying_testClass如何内部实现

1.4.2 NSKVONotifying_testClass如何内部实现

- setName:最主要的重写方法,set值时调用通知函数
- class:返回原来类的class
- dealloc
- _isKVOA判断这个类有没有被KVO动态生成子类

- (void)setClassName:(NSString *)className {

}

- (Class)class {
- 这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。
    return [testClass class];
}

- (void)dealloc {
    // 收尾工作
}

- (BOOL)_isKVOA {
- 添加一个名为_isKVOA的实例变量**,用于标识该对象是否支持KVO机制。
    return YES;
}


isa指向中间类之后如何调用方法:请添加图片描述
对于这两个属性 我们只监听了className属性而没有监听testArray属性
请添加图片描述

  • 调用监听的属性的设置方法,例如:setClassName:,都会先调用NSKVONotify_testClass对应的属性设置方法
  • 调用非监听属性的设置方法,如setClassArray方法,就会通过NSKVONotify_ApplesuperClass来找到testClass类对象,在调用其Apple类对象中的test方法

重写Class方法 :这是为了保证该中间类在外部使用时可以替代原始类,实现完全透明的KVO功能。

添加一个名为_isKVOA的实例变量,用于标识该对象是否支持KVO机制。

1.4.3 _NSSetObjectValueAndNotify

在具体实现过程中,系统会动态生成一个继承自原始类的中间类,并且在该类的初始化方法中,调用了一个叫做_NSSetObjectValueAndNotify()的函数,用于实现属性改变的通知。

_NSSetObjectValueAndNotify()函数的实现过程如下:

a) 首先会调用 willChangeValueForKey

b) 然后给属性赋值

c) 最后调用 didChangeValueForKey

d) 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .

1.5 总结KVO

1.5.1 KVO的本质

  • 利用runtime的API动态生成一个子类,并让实例对象的isa指向这个全新的子类
  • 当修改实例变量对象的属性时候,在全新子类的set方法中会调用Foundation的_NSSetXXXValueAndNotify函数
    willChangeValueForKey
  • 调用原来的setter
  • didChangeValueForKey:内部会触发监听器的监听方法

1.5.2 KVO使用场景

  • 对于时刻变化的对象,例如colletionView的items,总是动态的变化,这个时候可以使用KVO监听对象。
  • 在AVFounditon中获取AVPlayer的播放进度,播放状态,也需要使用KVO来观察。

1.5.3 实现过程总结

  • addObserver:forKeyPath:options:context:context调用的时候,会自动生成并注册一个该对象(被观察的对象)对应类的子类,取名NSKVONotify_Class,并且将该对象的isa指针指向这个新的类。
  • 在该子类内部实现4个方法-被观察属性的set方法、class方法、isKVO、delloc。
  • 最关键的是set方法中,先调用willChangeValueForKey,再给成员变量赋值,最后调用didChangeValueForKeywillChangeValueForKey和didChangeValueForKey需要成对出现才能生效,在didChangeValueForKey中会去调用观察者的observeValueForKeyPath: ofObject: 方法。
  • 重写class方法,这样避免外部感知子类的存在,同时防止在一些使用isKindOfClass判断的时候出错。
  • isKVO方法作为能否实现KVO功能的一个标识。
  • delloc里面还原isa指针

KVO还很多内部的类和实现 先学会实现,知道实现的过程,接着学习源码和之前的实现相对应学习会更好。

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

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

相关文章

Kafka基础概念介绍

背景 Kafka是我们项目用的最多的消息中间件,但里面也有很多存在的问题,如重复消费、带宽瓶颈、部分分区消费不下来的异常场景。 重复消费问题有些让人头疼(reblance导致offset提交失败),这里会持续更新(基…

UML简介与类图详解

1 UML简介 1.1 UML是什么 UML,全称为Unified Model Language,即统一建模语言,是由一整套图表组成的,为面向对象系统的产品进行说明、可视化和编制文档的一种标准语言。UML 代表了一组最佳工程实践,这些实践已被证明在…

【路径规划】基于哈里斯鹰优化算法的栅格法路径规划 机器人路径规划【Matlab代码#20】

文章目录 1. 原始HHO算法2. 机器人路径规划环境创建3. 路径规划模型建立4. 部分代码展示5. 仿真结果展示6. 资源获取方式 1. 原始HHO算法 详细介绍此处略,可参考HHO算法介绍 2. 机器人路径规划环境创建 对机器人工作空间的进行环境建模是机器人路径规划研究的重要…

【电源专题】案例:拆一个早期用的万能充看看内部状况

说到万能充,可能存在80/90后的记忆里。以前手机都可以更换电池的,所有往往都是买两块,一块在手机内部使用,另一块在万能充充电。 万能充的外观也是大同小异的,从网上找了一些图片: 这东西现在日常生活已经都看不到了,关键因素之一应该是现在的手机电池都是不可拆…

多维图像去噪方法研究

一、背景介绍 由于传感器技术的快速发展,高光谱(HS)遥感(RS)成像为飞机等数据采集设备远距离观测和分析地球表面提供了大量的空间和光谱信息,航天器和卫星。 HS RS 技术的最新进展甚至革命为实现各种应用的…

【ONE·C++ || set和map(一)】

总言 主要介绍set和map的基本框架和使用 文章目录 总言1、部分接口介绍和使用举例1.1、序列式容器和关联式容器、键值对1.1.2、pair键值对 1.2、set基本介绍1.2.1、set::set、遍历1.2.2、set::insert、set::erase、set::find1.2.3、set::count、set::lower_bound、set::upper_…

数据结构与算法基础(青岛大学-王卓)(1)

士别三日当刮目相待,不好意思鸽了好久了,因为学习的时间不连续,所以我一直攒着,我又回来继续更新了 没有继续学习浙大的数据结构了,对比了青岛大学的王老师的这个教程我觉得更适合我一些,更入门&#xff0…

【Spring篇】Spring整合

🍓系列专栏:Spring系列专栏 🍉个人主页:个人主页 目录 一、Spring整合 1.Spring整合Mybatis思路分析 1.环境准备 2.整合思路分析 2.Spring整合Mybatis 3.Spring整合Junit 1.环境准备 2.整合Junit步骤 二、图书推荐 1.《元宇宙Ⅱ:图…

Ubuntu安装MySQL

一.安装MySQL服务器 安装MySQL服务器: apt-get install mysql-server 对MySQL进行初始化,设置密码: mysql_secure_installation 注意,这里要设置密码的最低长度为8位,如果你设的密码小于8位,则会提示&am…

有趣工具合集小程序-做你的小树洞小程序

有趣工具合集小程序-做你的小树洞 今天闲来无事,发现了一个有趣的小程序-做你的小树洞,包含ChatGpt小机器人、抛硬币、手持弹幕、亲戚计算器、藏头诗、唐诗三百首、歇后语以及猜谜语等功能 小程序总体界面是这样的 1.藏头诗 这个小程序里边有很多有趣…

Redis的哨兵和集群模式

哨兵模式# 哨兵模式是redis高可用的实现方式之一 使用一个或者多个哨兵(Sentinel)实例组成的系统,对redis节点进行监控,在主节点出现故障的情况下,能将从节点中的一个升级为主节点,进行故障转义,保证系统的可用性。 哨…

【Qt5】多线程串口

文章目录 原版代码工程增加QCustomplot实时画图的源码工程源码 原版代码工程 源码下载链接: 链接:https://pan.baidu.com/s/15pWzadPwOx_OfJGtvL-MjA 提取码:lief –来自百度网盘超级会员V5的分享 增加QCustomplot实时画图的源码工程 源码&…

【Mybatis】增删改查

1.添加相应的jar包 2.创建持久化类 在src目录下创建一个名为com.mybatis.po的包 创建持久化类MyUser,包含三个属性(uid,uname,usex) package com.mybatis.po; /***springtest数据库中user表的持久化类*/ public class MyUser {private Integer uid;//主键private…

滴水逆向三期笔记与作业——02C语言——02数据类型

海哥牛逼 这里写自定义目录标题 一、C语言如何变成汇编1、裸函数 二、调用约定1、常见的几种调用约定 三、程序的真正入口四、数据类型4.1 C语言中的数据类型 作业 一、C语言如何变成汇编 1、裸函数 裸函数使用特殊方式定义,编译器和连接器并不会为其生成提升堆栈…

华为网工实验(VRRP多网关负载分担,OSPF基础操作)

采用VRRP多网关负载分担实现流量的负载均衡 配置思路:首先配置各个接口ip,让设备间能够实现通信,采用OSPF协议实现通信,然后AR2 AR3创建两个备份组,主备不同的两个备份组 组网图 #先设备命名并配置IP,三台设备类似&a…

路由器拨号密码恢复

背景 路由器拨号上网的密码因时间久远遗忘了,恢复并记录下过程。 步骤 1,安装wireshark choco install wireshark -y 注意 实践发现wireshark安装后,启动提示还需要安装Npcap 或者 Winpcap, 不过,在winpcap官网提示它已不再开…

虚拟机的克隆

第一步: 虚拟机右击 -> 管理 ->克隆 第二步: 在这一步勾选创建完整克隆,其他的都下一步 第三步 1.修改 mac地址 点击生成,就会生成一个随机的mac地址 2.修改主机名 vim /etc/hostname 修改完主机名后 reboot 重启虚拟机…

C++ STL学习之【优先级队列】

✨个人主页: 北 海 🎉所属专栏: C修行之路 🎃操作环境: Visual Studio 2019 版本 16.11.17 文章目录 🌇前言🏙️正文1、优先级队列的使用1.1、基本功能1.2、优先级模式切换1.3、相关题目 2、模拟…

蛋白质界的 ChatGPT:AlphaFold1 论文必备知识,不会有人还不知道吧

你知道 AlphaFold2 吗?它真正解决了蛋白质三维结构预测的算法困境,堪称蛋白质界的 chat-GPT4,甚至它的意义不是 chat-GPT4 所能够匹敌的。它为世界疾病治疗药物开发以及探究生物生命之谜提供了通向天神的一条道路,未来是生物的世纪…

Android开机时间工具分析

背景 android 上面有很多的方法可以分析开机时间 比如打log,通过log 分析。android 的官网上面提供了下面的两种图形化的方式来分析开机时间,一些异常很明显的拖长整个开机时间的活动 可以很容易就看出来。 问题 android 官网和网上的教程很多都不适用于…