iOS ------ KVO KVC

一, KVO

KVO介绍

  • KVO全称KeyValueObserving,俗称键值监听,是苹果提供的一套时事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接受事件。一般继承自NSObject的对象都默认支持KVO
  • KVO和NSNotificationCenter都是iOS观察者模式的一种实现。KVO对被监听对象无侵入性,不需要修改内部代码可以实现监听

实现原理

  • KVO是通过isa_swizzing技术实现的
  • 在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。当修改instance对象的属性时,会调用Foundation框架的_NSSetXXXValueAndNotify函数,该函数里面会先调用willChangeValueForkey:然后调用父类的setter方法修改值,最后是didChangeForKey:。didChangeValueForKey内部会触发监听器(Overseer) 的监听方法observeValueForKeyPath:ofObject:context:
  • 并且将class方法重写,返回原类的Class

KVO的使用

1.通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接受keyPath属性的变化事件

  • observer:观察者,监听属性变化的对象
  • keyPath:要观察的属性名称。要和属性声明的名称一致
  • options:回调方法中收到被观察者的属性的旧值或新值等 。对KVO机制进行配置,修改KVO通知的时机以及通知的内容
  • context:传入任意类型的对象,在接受消息回调的代码中可以接受到这个对象,是KVO中的一致传值方式

2.在观察者中实现observeValueForKeyPath:ofObject:change:context方法,当keyPath属性发生改变后,KVO会回调这个方法通知观察者

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

3.当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除

  • 调用removeObserver需要在观察者消失之前,否则会导致Crash。
  • 如果已经移除了监听,如果再次移除的时候,就会crash

对类对象进行验证

- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.[self setNameKVO];
}- (void)setNameKVO {self.person = [[Person alloc] init];self.person2 = [[Person alloc] init];NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@"监听到%@的%@属性值改变了 - %@ - %@",object, keyPath, change, context);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {self.person.name = @"cccc";self.person2.name = @"aaaa";
}- (void) dealloc {[self.person removeObserver:self forKeyPath:@"name"];
}

点击屏幕,打印结果

2024-07-29 15:19:56.184170+0800 KVO[43833:2012363] 监听到<Person: 0x600001ba2580>的name属性值改变了 - {kind = 1;new = cccc;old = "<null>";
} - 1111

KVO是通过isa_swizzing技术实现的。我们通过打断点,打印person和person2的isa指针。

(lldb) po self.person->isa
0x020060000383c843(lldb) po self.person2->isa
Person

通过打印我们知道两者的类对象并不相同,但person具体的类对象并没有打印出来。

导入runtime我们在注册观察者前后对两者的类进行打印

NSLog(@"person添加KVO监听之前 - %@ %@",object_getClass(self.person) , object_getClass(self.person2));NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];NSLog(@"person添加KVO监听之前 - %@ %@", object_getClass(self.person), object_getClass(self.person2));

打印结果

2024-07-29 15:19:52.524876+0800 KVO[43833:2012363] person添加KVO监听之前 - Person Person
2024-07-29 15:19:52.525004+0800 KVO[43833:2012363] person添加KVO监听之前 - NSKVONotifying_Person Person

添加KVO监听之后,person的类对象变为了NSKVONotifying_YZPerson,这是苹果为我们生成的中间类。

对setter方法IMP进行验证

当改变name属性的时候,是调用setName:进行的,我们来看看setName:有什么变化

    NSLog(@"person添加KVO监听之前 - %p %p",[self.person methodForSelector:@selector(setName:)], [self.person2 methodForSelector:@selector(setName:)]);NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];NSLog(@"person添加KVO监听之后 - %p %p",[self.person methodForSelector:@selector(setName:)], [self.person2 methodForSelector:@selector(setName:)]);

打印结果

2024-07-29 15:29:08.844353+0800 KVO[43989:2019265] person添加KVO监听之前 - 0x1043b5ab8 0x1043b5ab8
2024-07-29 15:29:08.844517+0800 KVO[43989:2019265] person添加KVO监听之后 - 0x180b60f08 0x1043b5ab8

添加监听后,self.person的setName的地址发生了改变

进一步打断点获取详细信息

2024-07-29 15:35:05.080958+0800 KVO[44095:2024193] person添加KVO监听之前 - 0x1041d1ab8 0x1041d1ab8
2024-07-29 15:35:05.081123+0800 KVO[44095:2024193] person添加KVO监听之后 - 0x180b60f08 0x1041d1ab8
(lldb) po (IMP) 0x1041d1ab8
(KVO`-[Person setName:] at Person.h:14)
(lldb) po (IMP) 0x180b60f08
(Foundation`_NSSetObjectValueAndNotify)

可以看到添加KVO监听之后,setName:方法的IMP指向了Fondation框架下的_NSSetObjectValueAndNotify

内部调用流程

设置了kvo监听之后,内部调用有什么流程。我们在Person中添加如下代码


#import "YZPerson.h"@implementation YZPerson
- (void)setName:(NSString *)name{_name = name;   
}- (void)willChangeValueForKey:(NSString *)key
{[super willChangeValueForKey:key];NSLog(@"willChangeValueForKey");
}- (void)didChangeValueForKey:(NSString *)key
{NSLog(@"didChangeValueForKey - begin");[super didChangeValueForKey:key];NSLog(@"didChangeValueForKey - end");
}

打印结果

2024-07-29 15:46:31.300565+0800 KVO[44318:2032881] willChangeValueForKey
2024-07-29 15:46:31.300689+0800 KVO[44318:2032881] didChangeValueForKey - begin
2024-07-29 15:46:31.301102+0800 KVO[44318:2032881] 监听到<Person: 0x600001662480>的name属性值改变了 - {kind = 1;new = cccc;old = "<null>";
} - 1111
2024-07-29 15:46:31.301192+0800 KVO[44318:2032881] didChangeValueForKey - end

发现在调用 [super didChangeValueForKey:key];的时候,监听到对象的改变,进而处理监听逻辑

窥探 NSKVONotifying_Person 的方法

- (void)printMethodNamesOfClass:(Class)cls
{unsigned int count;// 获得方法数组Method *methodList = class_copyMethodList(cls, &count);// 存储方法名NSMutableString *methodNames = [NSMutableString string];// 遍历所有的方法for (int i = 0; i < count; i++) {// 获得方法Method method = methodList[i];// 获得方法名NSString *methodName = NSStringFromSelector(method_getName(method));// 拼接方法名[methodNames appendString:methodName];[methodNames appendString:@", "];}// 释放free(methodList);// 打印方法名NSLog(@"%@ %@", cls, methodNames);
}-(void)setNameKVO{self.person = [[YZPerson alloc] init];// 注册观察者NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];NSLog(@"person添加KVO监听之后 - self.person的类是:%@   里面的方法有:",object_getClass(self.person));[self printMethodNamesOfClass:object_getClass(self.person)];
}

打印结果

2024-07-29 16:11:14.569381+0800 KVO[44880:2053144] NSKVONotifying_Person setName:, class, dealloc, _isKVOA,

系统重写了新建的子类 NSKVONotifying_Person 的setName, class, dealloc,新增了 _isKVOA方法。

setName

在NSKVONotifying_Person ,系统会重写这个方法实现属性变化的通知

- (void)setName:(NSString *)name {[self willChangeValueForKey:@"name"];[super setName:name];[self didChangeValueForKey:@"name"];
}

作用:willChangeValueForKey:didChangeValueForKey:会在属性变化之前和之后调用,触发KVO通知机制。观察者会在didChangeValueForKey:调用时收到通知。

class

- (Class)class {return [YZPerson class];
}

重写class方法,使其返回原始类Person,而不是其真实动态生成的KVO子类。保持了对象的分装性和透明性

dealloc

- (void)dealloc {// 通常会调用移除所有观察者的代码[self removeObserver:self forKeyPath:@"name"];// 清理其他资源[super dealloc];
}

确保在对象销毁前,所有的KVO观察者都被正确的移除,防止因为访问已经释放的对象而导致崩溃

_isKVOA

- (BOOL)_isKVOA {return YES;
}

新增的私有方法,由于标识对象是否被KVO代理

手动调用KVO

KVO监听的关键 willChangeValueForKeydidChangeValueForKey 起了关键作用,一般来说只有监听属性发生变化的时候,才能触发监听,但是如果我们想自己手动调用KVO的话,只要自己手动调用这两个方法就可以了。

-(void)setNameKVO{self.person = [[YZPerson alloc] init];// 注册观察者NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.person addObserver:self forKeyPath:@"name" options:options context:@"1111"];NSLog(@"person添加KVO监听之后 - self.person的类是:%@   里面的方法有:",object_getClass(self.person));[self printMethodNamesOfClass:object_getClass(self.person)];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{//    self.person.name = @"ccc";// 手动调用KVO[self.person willChangeValueForKey:@"name"];[self.person didChangeValueForKey:@"name"];
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
- (void)dealloc
{// 移除监听[self.person removeObserver:self forKeyPath:@"name"];
}

打印结果

2024-07-29 16:44:50.179131+0800 KVO[45535:2079896] 监听到<Person: 0x60000350cf80>的name属性值改变了 - {kind = 1;new = "<null>";old = "<null>";
} - 1111

用于new = null,也就是name的值没有改变,我们手动调用才触发的监听。

KVC

  • KVC是Key Value Coding的简称。它是一种痛过字符串的名字(key)来访问类属性的机制。而不是通过Setter和Getter方法访问。KVC的方法定义在Foundation/NSKeyValueCoding中。
  • KVO和KVC都属于键值编程而底层实现机制都是isa_swizzing

常见的API有

-(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
-(void)setValue:(id)value forKey:(NSString *)key;
-(id)valueForKeyPath:(NSString *)keyPath;
-(id)valueForKey:(NSString *)key;

key和keyPath的区别

key:只能访问对象的当前层级属性
keyPath:可以访问对象层次结构中的嵌套属性

多值操作
给定一组Key,获得一组value,以字典的形式返回。该方法为数组中的每个Key调用valueForKey:方法。

- (NSDictionary<NSString *,id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

将指定字典中的值设置到消息接收者的属性中,使用字典的Key标识属性。默认实现是为每个键值对调用setValue:forKey:方法 ,会根据需要用nil替换NSNull对象

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;

例子


@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@property (nonatomic, strong) NSString *sex;@end
Person* person = [[Person alloc] init];NSDictionary* dictionary = @{@"name":@"fu",@"age":@66,@"sex":@"sex"};[person setValuesForKeysWithDictionary:dictionary];NSLog(@"name:%@",person.name);NSLog(@"age:%@",person.age);NSLog(@"sex:%@",person.sex);NSDictionary* dictionary1 = [person dictionaryWithValuesForKeys:@[@"name",@"age",@"sex"]];NSLog(@"Dictionary : %@", dictionary1);

输出

2024-07-29 21:24:40.266469+0800 KVC1.0[51192:2260944] model.name:fu
2024-07-29 21:24:40.266530+0800 KVC1.0[51192:2260944] model.age:66
2024-07-29 21:24:40.266572+0800 KVC1.0[51192:2260944] model.sex:sex
2024-07-29 21:24:40.266640+0800 KVC1.0[51192:2260944] tempModelDictionary : {age = 66;name = fu;sex = sex;
}

集合类型

FXPerson *person = [FXPerson new];// 赋值
ThreeFloats floats = {180.0, 180.0, 18.0};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSLog(@"非对象类型%@", [person valueForKey:@"threeFloats"]);// 取值
ThreeFloats th;
NSValue *currentValue = [person valueForKey:@"threeFloats"];
[currentValue getValue:&th];
NSLog(@"非对象类型的值%f-%f-%f", th.x, th.y, th.z);

非对象处理
KVC支持基础数据类型和结构体,在使用KVC进行赋值或取值的时候,会自动在非对象值和对象值之间进行转换。

  • 当进行取值如valueForKey:时,如果返回值非对象,会使用该值初始化一个NSNumber(用于基础数据类型)或NSValue(用于结构体)实例,然后返回该实例。
  • 当进行赋值如setValue:forKey:时,如果key的数据类型非对象,则会发送一条<type>Value消息给value对象以提取基础数据,然后赋值给key。

赋值setValue:forKey的原理

  1. 按照setKey:_setKey:的顺序查找方法,如果找到方法就传递参数,调用方法
  2. 如果没有找到。查看accessInstanceVariablesDirectly方法的返回值如果返回NO,调用setValue:forUnderfinedKey:并抛出异常
  3. 如果返回YES,按照_key,_iskey,key,iskey的顺序查找成员变量。如果找到成员变量,就直接赋值
  4. 如果没找到,就调用setValue:forUnderfinedKey:并抛出异常

在这里插入图片描述

取值 valueForKey:的原理

1,按照getKeykeyisKey_key的顺序查找方法,如果找到了,就直接调用
2,如果没找到,就查看accessInstanceVariablesDirectly 方法的返回值,如果返回NO 调用valueForUndefinedKey:并抛出异常NSUnknownKeyException
3,YES 按照_key_isKeykeyisKey的顺序查找成员变量
如果找到了成员变量,就直接取值。
4,如果没有查找到成员变量就调用valueForUndefinedKey:并抛出异常NSUnknownKeyException

在这里插入图片描述

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

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

相关文章

MySQL基础练习题11-换座位

题目&#xff1a;交换每两个连续的学生的座位号。如果学生的数量是奇数&#xff0c;则最后一个学生的id不交换。按 id 升序 返回结果表。 准备数据 分析数据 方法一&#xff1a;利用power函数对id进行交换&#xff0c;得出的答案只有0或1 第一步&#xff1a;用power()函数将…

公司常用的监控软件有哪些?2024年六大公司监控软件良心推荐!

在现代企业管理中&#xff0c;监控软件不仅可以帮助提高员工生产力&#xff0c;还可以确保企业数据的安全和保护。小编分享六款公司监控软件&#xff0c;能够满足不同企业的需求&#xff0c;提升管理效率和信息安全。 一、值得推荐的监控软件 1. 固信软件 固信软件https://ww…

【软件测试】--接口测试

1. 接口用例设计 接口测试的测试点 功能测试 单接口功能&#xff1a; 手工测试中的单个业务模块&#xff0c;一般对应一个接口 登陆业务 --> 登陆接口加入购物车业务 --> 加入购物车接口订单业务 --> 订单接口支付业务 --> 支付接口 借助工具、代码。绕开前端界面…

【初阶数据结构题目】1.返回倒数第k个节点

文章目录 题目描述代码 题目描述 返回倒数第k个节点 代码 /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/typedef struct ListNode ListNode; int kthToLast(struct ListNode* head, int k){ListNode* t hea…

域名未备案可以申请SSL证书吗??

域名未备案可以申请SSL证书。SSL证书的申请并不依赖于域名的备案情况&#xff0c;而是在于对域名的掌握权。只要你拥有域名的所有权&#xff0c;即具有对域名管理的权限&#xff0c;就可以在任何时候向认证机构申请SSL证书。 申请SSL证书的流程通常包括以下几个步骤&#xff1a…

做前端4年了,才明白技术的本质不过是工具而已

四年前&#xff0c;我踏上了前端开发的道路&#xff0c;从HTML和CSS到JavaScript&#xff0c;从jQuery到React&#xff0c;每一步都走得踏实而坚定。随着经验的积累&#xff0c;技术的进步&#xff0c;我逐渐认识到&#xff0c;所谓的“技术”&#xff0c;无非是实现目标的一种…

颜色识别基于高斯混合模型(GMM)的查找表分类器(LUT)

文章目录 create_class_gmm 创建高斯混合模型&#xff08;GMM&#xff09;以进行分类任务add_samples_image_class_gmm 提取训练样本&#xff0c;并将其添加到高斯混合模型 (GMM) 的训练数据集中train_class_gmm 训练一个高斯混合模型 (GMM)clear_class_gmm 清除模型create_cla…

Fiddler学习笔记

目录 前言 简介 原理 界面 前言 测试可以使用fiddler工具&#xff0c;通过抓包的方式修改前端参数和模拟后端返回&#xff0c;快速定位缺陷。 简介 Fiddler是HTTP协议调试代理工具&#xff0c;可以记录并检查所有客户端和服务器之间的HTTP和HTTPS请求&#xff0c;允许监视…

QT报红色错误,实际可以编译

QT报红色错误&#xff0c;实际可以编译&#xff0c;看着难受&#xff0c;如何去掉报警 进入插件 勾选框去掉&#xff0c;然后重启QT

Java——循环控制for,while,do...while

目录 1.for循环控制 基本介绍 基本语法 流程分析 案例演示&#xff1a; 注意事项和细节说明 练习题 2.while循环控制 基本语法 流程图 案例演示1 注意事项和细节说明 案例演示2 ​3.do...while循环控制 基本语法 说明 流程图 注意事项和细节说明 练习题…

杂项运算符及运算符的优先级

文章目录 常见的杂项运算符运算符的优先级特殊运算符运算符重载运算符的结合性实际应用中的注意事项1. 空条件运算符 (Null Coalescing Operator)JavaScript 示例: 2. 范围运算符 (Range Operator)Swift 示例: 3. 模式匹配运算符 (Pattern Matching)Rust 示例: 4. 解构赋值运算…

C# 12 新增功能实操!

前言 今天咱们一起来探索并实践 C# 12 引入的全新功能&#xff01; C#/.NET该如何自学入门&#xff1f; 注意&#xff1a;使用这些功能需要使用最新的 Visual Studio 2022 版本或安装 .NET 8 SDK 。 主构造函数 主构造函数允许你直接在类定义中声明构造函数参数&#xff0c;…

从零开始编写一个Chrome插件:详细教程

个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[2435024119@qq.com] 📱个人微信:15279484656 🌐个人导航网站:www.forff.top 💡座右铭:总有人要赢。为什么不能是我呢? 专栏导…

Pytorch笔记1

建议点赞收藏关注&#xff01;持续更新至pytorch大部分内容更完。 整体框架如下 目录 gpu加速数据数据结构张量TensorVariable 预处理数据增强 模型构建模块组织复杂网络初始化网络参数定义网络层 损失函数创建损失函数设置损失函数超参数选择损失函数 优化器管理模型参数管理…

“八股文”:程序员的福音还是梦魇?

——一场关于面试题的“代码战争” 在程序员的世界里&#xff0c;“八股文”这个词儿可谓是“如雷贯耳”。不&#xff0c;咱们可不是说古代科举考试中的那种八股文&#xff0c;而是指程序员面试中的那些固定套路的题目。如今&#xff0c;各大中小企业在招聘程序员时&#xff0…

Overlay网络

Overlay 介绍 Overlay网络是将已有的物理网络&#xff08;Underlay网络&#xff09;作为基础&#xff0c;在其上建立叠加的逻辑网络&#xff0c;实现网络资源的虚拟化。 传统网络带来了以下一些问题&#xff1a; ● 虚拟机规模受 网络规格限制在传统二层网络环境下&#xff0…

泰迪智能科技大数据实验室——陕西省高校合作成功案例

近年来&#xff0c;陕西省紧跟国家大数据发展战略&#xff0c;积极推进大数据产业发展。在政策扶持、产业布局、技术创新等方面取得显著成效。泰迪智能科技大数据实验室立足陕西&#xff0c;携手西安邮电大学、西安财经大学、陕西科技大学镐京学院、宝鸡文理学院、渭南师范学院…

使用Selenium爬虫批量下载AlphaFold数据库中的PDB文件

注意&#xff1a;本方法使用了python&#xff0c;下载速度一般&#xff0c;如果需要更快的大批量下载可以考虑使用其他方法&#xff0c;例如FTP Alphafold数据库其实提供了许多物种的蛋白质组&#xff1a; AlphaFold Protein Structure Database 但是如果你搜索的物种不在这个…

【从0制作自己的ros导航小车:上位机篇】02、ros1多机通讯与坐标变换可视化

从0制作自己的ros导航小车 前言一、ros1多机通讯二、rviz可视化小车坐标系 前言 上节课完成了里程计数据与坐标变换发布&#xff0c;但是还没有测试&#xff0c;本节进行测试&#xff0c;测试之前需要知道一件事&#xff0c;上位机也就是开发板一般不做可视化用&#xff0c;因…

Python 教程(七):match...case 模式匹配

目录 专栏列表前言基本语法match 语句case 语句 模式匹配的类型示例具体值匹配类型匹配序列匹配星号表达式命名变量复杂匹配 模式匹配的优势总结 专栏列表 Python教程&#xff08;一&#xff09;&#xff1a;环境搭建及PyCharm安装Python 教程&#xff08;二&#xff09;&…