【iOS】KVOKVC原理

1 KVO 键值监听

1.1 KVO简介

KVO的全称是Key-Value Observing,俗称"键值监听",可以用于监听摸个对象属性值得改变。

KVO一般通过以下三个步骤使用:

// 1. 添加监听
[self.student1 addObserver:self forKeyPath:@"age" options:options context:nil];// 2. 重写- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@"%@的%@被改变:%@", object, keyPath, change);
}// 3. 适当时机移除监听
[self.student1 removeObserver:self forKeyPath:@"age"];

1.2 KVO简单使用

  1. 建立SXStudent类和SXTeacher
//SXStudent.h #import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface SXStudent : NSObject
@property (nonatomic, assign) NSInteger age;
@endNS_ASSUME_NONNULL_END// SXTeacher.h
#import <Foundation/Foundation.h>
#import "SXStudent.h"NS_ASSUME_NONNULL_BEGIN@interface SXTeacher : NSObject
@property (nonatomic, strong) SXStudent *student1;
@property (nonatomic, strong) SXStudent *student2;
- (void)demo;
@endNS_ASSUME_NONNULL_END
  1. 实现SXStudent类。
// SXStudent.m#import "SXStudent.h"
@implementation SXStudent
@end
  1. 实现SXTeacher类,重写init方法,为SXTeacherstudent1属性添加监听。实现demo方法,分别更改student1student2age值。
// SXTeacher.m#import "SXTeacher.h"
#import <objc/runtime.h>@implementation SXTeacher- (id)init {if (self = [super init]) {self.student1 = [[SXStudent alloc] init];self.student2 = [[SXStudent alloc] init];self.student1.age = 1;self.student2.age = 2;// 添加监听NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.student1 addObserver:self forKeyPath:@"age" options:options context:nil];}return self;
}- (void)demo {self.student1.age = 20;self.student2.age = 30;
}- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@"%@的%@被改变:%@", object, keyPath, change);
}- (void)dealloc {// 移除监听[self.student1 removeObserver:self forKeyPath:@"age"];
}@end
  1. mian函数内创建SXTeacher的实例对象并调用demo方法测试。
#import <Foundation/Foundation.h>
#import "SXTeacher.h"int main(int argc, const char * argv[]) {@autoreleasepool {SXTeacher *teacher = [[SXTeacher alloc] init];[teacher demo];}return 0;
}
  1. 查看运行结果。
    在这里插入图片描述

1.3 实现原理探究

1.3.1 student1发生的变化

为什么student1setter方法可以触发监听,添加监听的方法到底对student1做了什么?

  1. 我们在添加监听后打一个断点。
    在这里插入图片描述
  2. 试着利用lldb调试查看student1student2isa指针。
    在这里插入图片描述

我们发现student1isa指针的指向被更改成了NSKVONotifying_SXStudent(NSKVONotifying_为前缀,原类名为后缀)类。

1.3.2 NSKVONotifying_XXX类

  1. 关于NSKVONotifying_XXX
  • NSKVONotifying_XXX类是 Runtime动态创建的一个类,在程序运行的过程中产生的一个新的类。
  • NSKVONotifying_XXX类是原类的一个子类。
  • NSKVONotifying_XXX类存在自己的 setAge:classdeallocisKVOA…方法。

试着验证NSKVONotifying_XXX类的方法和父类,我们可以使用如下代码打印NSKVONotifying_SXStudent类和SXStudent类的方法列表和父类类型。

- (void)demo2 {[self printMethods:object_getClass(self.student1)];[self printMethods:object_getClass(self.student2)];
}- (void) printMethods:(Class)cls {unsigned int count;Method *methods = class_copyMethodList(cls, &count);NSMutableString *methodNames = [NSMutableString string];[methodNames appendFormat:@"%@ - ", cls];NSLog(@"%@ superClass ----> %@", NSStringFromClass(cls), NSStringFromClass(class_getSuperclass(cls)));for (int i = 0; i < count; i++) {Method method = methods[i];NSString *methodName = NSStringFromSelector(method_getName(method));[methodNames appendFormat:@"%@ ", methodName];}NSLog(@"%@", methodNames);free(methods);
}

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

可以看到NSKVONotifying_SXStudent类有自己的setAge:classdealloc_isKVOA方法。

  • 重写class方法是为了隐藏NSKVONotifying_XXX类的存在。重写后的class方法返回其父类(原来的类)类型,使用户以为类没有变化。
  • _isKVOA则是用来标识当前类是否是通过runtime动态生成的类对象,如果是,就返回YES,不是,则返回NO。
  • 当对像被销毁后,dealloc做一些收尾工作。

1.3.3 方法调用探究

由上面可分析出我们的student1isa指针指向的类对象是NSKVONotifying_SXStudent,并且NSKVONotifying_SXStudent中还带有setAge: 方法,所以student1setAge:方法走的应该是NSKVONotifying_SXStudent类中的setAge:方法。

我们试着使用下面的代码打印student1被监听前后的setAge:方法的地址,并使用lldb调试一探究竟。

- (id)init {if (self = [super init]) {self.student1 = [[SXStudent alloc] init];self.student2 = [[SXStudent alloc] init];self.student1.age = 1;self.student2.age = 2;NSLog(@"添加监听之前 - p1 = %p, p2 = %p", [self.student1 methodForSelector:@selector(setAge:)], [self.student2 methodForSelector:@selector(setAge:)]);// 添加监听NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.student1 addObserver:self forKeyPath:@"age" options:options context:nil];NSLog(@"添加监听之后 - p1 = %p, p2 = %p", [self.student1 methodForSelector:@selector(setAge:)], [self.student2 methodForSelector:@selector(setAge:)]);}return self;
}

打断点使用lldb打印方法地址对应的方法名:
在这里插入图片描述

我们发现student1setAge:方法实际上是调用了Foundation框架的_NSSetLongLongValueAndNotify函数。这又是怎么回事,我们先来了解一下这个函数。

1.3.3 _NSSetXXXValueAndNotify

经过查阅资料我们可以了解到。
NSKVONotifyin_XXX中的setage:方法中其实调用了 Fundation框架中C语言函数 _NSsetXXXValueAndNotify_NSsetXXXValueAndNotify内部做的操作相当于,首先调用willChangeValueForKey 将要改变方法,之后调用原来的setage方法对成员变量赋值,最后调用didChangeValueForKey已经改变方法。didChangeValueForKey中会调用监听器的监听方法,最终来到监听者的observeValueForKeyPath方法中。

Foundation框架中会根据属性的类型,调用不同的方法。例如我们之前定义的NSInteger类型的age属性,那么我们看到Foundation框架中调用的_NSsetLongLongValueAndNotify函数。那么我们把age的属性类型变为double重新打印一遍。

在这里插入图片描述

我们发现调用的函数变为了_NSSetDoubleValueAndNotify,那么这说明Foundation框架中有许多此类型的函数,通过属性的不同类型调用不同的函数。

我们可以重写 SXStudent类的willChangeValueForKey方法和didChangeValueForKey方法来验证上述说法。

#import "SXStudent.h"@implementation SXStudent
- (void)setAge:(NSInteger)age {NSLog(@"setAge");_age = age;
}
- (void)willChangeValueForKey:(NSString *)key {NSLog(@"willChangeValueForKey begin");[super willChangeValueForKey:key];NSLog(@"willChangeValueForKey end");
}- (void)didChangeValueForKey:(NSString *)key {NSLog(@"didChangeValueForKey begin");[super didChangeValueForKey:key];NSLog(@"didChangeValueForKey end");
}
@end

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

可知:

  • _NSSetXXXValueAndNotify调用willChangeValueForKey:
  • _NSSetXXXValueAndNotify调用setter实现;
  • _NSSetXXXValueAndNotify调用didChangeValueForKey:
  • didChangeValueForKey内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法。

1.3.4 伪代码

据上所述,可以写出NSKVONotifying_SXStudent类的伪代码:

///> NSKVONotifying_SXStudent.m 文件#import "NSKVONotifying_SXStudent.h"@implementation NSKVONotifying_SXStudent- (void)setAge:(int)age{_NSSetLongLongValueAndNotify();  ///> 文章末尾 知识点补充小结有此方法来源
}void _NSSetLongLongValueAndNotify(){[self willChangeValueForKey:@"age"];[super setAge:age];[self didChangeValueForKey:@"age"];
}- (void)didChangeValueForKey:(NSString *)key{///> 通知监听器 key发生了改变[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}@end

2 KVC 键值编码

2.1 KVC简介

KVC的全称key - value - coding,俗称"键值编码",可以通过key来访问某个属性。

常见的API有:

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

2.2 KVC简单使用

2.2.1 自定义SXDog类、SXStudent类和SXTeacher类。

// SXDog.h
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface SXDog : NSObject
@property (nonatomic, assign) CGFloat weight;
@endNS_ASSUME_NONNULL_END// SXStudent.h
#import <Foundation/Foundation.h>
#import "SXDog.h"NS_ASSUME_NONNULL_BEGIN@interface SXStudent : NSObject
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, strong) SXDog *dog;
@endNS_ASSUME_NONNULL_END// SXTeacher.h
#import <Foundation/Foundation.h>
#import "SXStudent.h"NS_ASSUME_NONNULL_BEGIN@interface SXTeacher : NSObject
@property (nonatomic, strong) SXStudent *student1;
@endNS_ASSUME_NONNULL_END

2.2.2 实现这三个类,为了方便使用,重写SXStudent和SXTeacher的初始化方法,在初始化方法里对属性进行初始化。

SXDog.m

#import "SXDog.h"@implementation SXDog
@end

SXStudent.m

#import "SXStudent.h"
#import <objc/runtime.h>@implementation SXStudent
- (id)init {if (self = [super init]) {self.dog = [[SXDog alloc] init];}return self;
}
@end

SXTeacher.m

#import "SXTeacher.h"
#import <objc/runtime.h>@implementation SXTeacher
- (id)init {if (self = [super init]) {self.student1 = [[SXStudent alloc] init];}return self;
}
@end

2.2.3 为SXTeacher添加两个方法,在这两个方法里试调用KVC。

  1. SetValue:ForKey:ValueForKey:方法
- (void)demoSetValueForKeyAndValueForKey {[self.student1 setValue:@20 forKey:@"age"];NSLog(@"点语法:%ld", self.student1.age);NSNumber *value = [self.student1 valueForKey:@"age"];NSLog(@"KVC:%@", value);
}
  1. SetValue:ForKeyPath:ValueForKeyPath:
- (void)demoSetValueForKeyPathAndValueForKeyPath {[self.student1 setValue:@16 forKeyPath:@"dog.weight"];NSLog(@"点语法:%lf", self.student1.dog.weight);NSNumber *value = [self.student1 valueForKeyPath:@"dog.weight"];NSLog(@"KVC:%@", value);
}
  1. 调用上面两个函数,运行。
    在这里插入图片描述

2.2.4 KeyPath 和 Key 的区别:

  • keyPath 相当于根据路径去寻找属性,一层一层往下找。
  • key 是直接访问属性的名字,如果按路径找会报错。

2.3 KVC流程

2.3.1 setValue:forkey:赋值流程

在这里插入图片描述

  • 首先会按照setKey:、_setKey:的顺序到对象的方法列表中寻找这两个方法,如果找到了方法,则传参并且调用方法。
  • 如果没有找到方法,则通过accessInstanceVariablesDirectly方法的返回值来决定是否能够查找成员变量。如果accessInstanceVariablesDirectly返回YES,则会按照以下顺序到成员变量列表中查找对应的成员变量:
    • _key
    • _isKey
    • key
    • isKey
  • 如果accessInstanceVariablesDirectly返回NO,则直接抛出NSUnknownKeyException异常。
    如果在成员变量列表中找到对应的属性值,则直接进行赋值,如果找不到,则会抛出NSUnknownKeyException异常。

accessInstanceVariablesDirectly函数

+ (BOOL)accessInstanceVariablesDirectly{return YES;   ///> 可以直接访问成员变量//    return NO;  ///>  不可以直接访问成员变量,  ///> 直接访问会报NSUnkonwKeyException错误  
}

2.3.2 valueForKey:取值流程

在这里插入图片描述

  • 首先会按照以下顺序查找方法列表:
    • getKey
    • key
    • isKey
    • _key
  • 如果找到就直接传递参数,调用方法,如果未找到则查看accessInstanceVariablesDirectly方法的返回值,如果返回NO,则直接抛出NSUnknownKeyException异常。
  • 如果accessInstanceVariablesDirectly方法返回YES,则按如下顺序查找成员变量列表:
    • _key
    • _isKey
    • key
    • isKey
  • 如果能找到对应的成员变量,则直接获取成员变量的值,如果未找到,则抛出NSUnknownKeyException异常。

2.3.3 试验证setValue:forkey:赋值流程

对上述例子进行小修改:
SXStudent.h

@interface SXStudent : NSObject {@publicint _age;int _isAge;int age;int isAge;
}
@end

SXTeacher.m

- (id)init {if (self = [super init]) {self.student1 = [[SXStudent alloc] init];NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.student1 addObserver:self forKeyPath:@"age" options:option context:nil];[self.student1 setValue:@20 forKey:@"age"];NSLog(@"-----");}return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {NSLog(@"%@的%@被改变:%@", object, keyPath, change);
}

NSLog(@"-----");处打下断点,运行,查看student1中的成员变量。看看谁被赋值了。
在这里插入图片描述

可以看到_age首先被赋值,我们注释掉SXStudent中的_age成员变量,看看下一个是谁被赋值。如此反复,就可以得到setValue:forkey:赋值流程。结果与上述无误,我就不继续了。

通过本例,我们还可以知道KVC也可以触发KVO监听。

3 一些问题

3.1 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

给一个实例对象添加KVO,系统内部是利用Runtime动态的生成一个此实例对象的类对象的子类,具体的格式为_NSKVONotifying_XXX,并且让实例对象的isa指针指向这个新生成的类。
重写属性的set方法,当调用set方法时,会调用Foundation框架的NSSetXXXValueAndNotify函数
_NSSetXXXValueAndNotify中会执行以下步骤:

  • 调用willChangeValueForKey:方法;
  • 调用父类的set方法,重新赋值;
  • 调用didChangeValueForKey:方法;
  • didChangeValueForKey:内部会触发监听器的observeValueForKeyPath:ofObject:change:context:方法。

3.2 如何手动触发KVO?

手动调用willChangeValueForKey:didChangeValueForKey:

例:

- (id)init {if (self = [super init]) {self.student1 = [[SXStudent alloc] init];NSKeyValueObservingOptions option = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;[self.student1 addObserver:self forKeyPath:@"age" options:option context:nil];[self.student1 willChangeValueForKey:@"age"];[self.student1 didChangeValueForKey:@"age"];}return self;
}

运行结果:

在这里插入图片描述

虽然是在didChangeValueForKey:内部会触发监听器的observeValueForKeyPath:ofObject:change:context:方法,但是如果不调用willChangeValueForKey:无法就无法触发监听器,这两个必须一起使用。

3.3 直接修改成员变量的值是否会触发KVO?

直接修改成员变量的值不会触发KVO,因为没有触发setter方法。

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

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

相关文章

protobuf数据类型

protobuf特殊数据类型 一&#xff0c;特殊数据类型1&#xff0c;enum使用规则注意事项 2&#xff0c;Any3&#xff0c;oneof4&#xff0c;map通讯录demo升级 二&#xff0c;默认值三&#xff0c;更新消息更新规则保留字段reserved未知字段获取未知字段 四&#xff0c;选项optio…

时频分析方法的matlab实现

傅里叶变换 function [ output_args ] example3_7( input_args ) %EXAMPLE3_7 Summary of this function goes here % Detailed explanation goes here clc; clear; fs12800;%采样频率 s1load(Sig1.txt); s2load(Sig2.txt); lslength(s1); figure(1) subplot(211) plot…

Cilium 系列-7-Cilium 的 NodePort 实现从 SNAT 改为 DSR

系列文章 Cilium 系列文章 前言 将 Kubernetes 的 CNI 从其他组件切换为 Cilium, 已经可以有效地提升网络的性能。但是通过对 Cilium 不同模式的切换/功能的启用&#xff0c;可以进一步提升 Cilium 的网络性能。具体调优项包括不限于&#xff1a; 启用本地路由 (Native Rou…

西安市未央区地方财政支出绩效管理研究_kaic

摘 要 目前传统的地方财政绩效管理研究普遍上主要集中在有关收入研究方面上&#xff0c;而对其支出的规模以及各类结构的研究较少。我国大部分地方财政政府的财政收入低下&#xff0c;财政支出效率有限&#xff0c;不能很好的为其地方经济提供较为稳定的社会支撑和经济保障。造…

SQL基础使用

SQL的概述 SQL全称&#xff1a; Structured Query Language&#xff0c;结构化查询语言&#xff0c;用于访问和处理数据库的标准的计算机语言。 SQL语言1974年由Boyce和Chamberlin提出&#xff0c;并首先在IBM公司研制的关系数据库系统SystemR上实现。 经过多年发…

PostgreSQL构建时间

– PostgreSQL构建时间 select make_timestamp(2023,7,27,7,34,16);

K8s-资源管理(二)

文章目录 2. 资源管理2.1 资源管理介绍2.2 YAML语言介绍2.3 资源管理方式2.3.1 命令式对象管理2.3.2 命令式对象配置2.3.3 声明式对象配置 2.4. 模拟使用普通用户来操作2.5 kubectl 一些基本命令2.6 使用个人的 docker 仓库的镜像 2. 资源管理 2.1 资源管理介绍 在kubernetes…

基于深度学习的高精度狗狗检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度狗狗检测识别系统可用于日常生活中检测与定位120类狗狗目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的狗狗目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检测模型训练…

web漏洞-java安全(41)

这个重点是讲关于java的代码审计&#xff0c;看这些漏洞是怎么在java代码里面产生的。 #Javaweb 代码分析-目录遍历安全问题 这个漏洞原因前面文章有&#xff0c;这次我们看看这个漏洞如何在代码中产生的&#xff0c;打开靶场 解题思路就是通过文件上传&#xff0c;上传文件…

如何利用tf.keras 实现深度学习?

tf.keras是TensorFlow 2.0的高阶API接口&#xff0c;为TensorFlow的代码提供了新的风格和设计模式&#xff0c;大大提升了TF代码的简洁性和复用性&#xff0c;官方也推荐使用tf.keras来进行模型设计和开发。 常用模块 tf.keras中常用模块如下表所示&#xff1a; 常用方法 深度…

安全学习DAY10_HTTP数据包

HTTP数据包 文章目录 HTTP数据包小节导图Request请求数据包结构Request请求方法&#xff08;方式&#xff09;请求头&#xff08;Header&#xff09;Response响应数据包结构Response响应数据包状态码状态码作用&#xff1a;部分状态码详解判断网站文件是否存在的状态码&#xf…

配置代理——解决跨域问题(详解)

之前写项目的时候总会遇到配置代理的问题&#xff0c;可是配置了之后有时有用&#xff0c;有时就没有用&#xff0c;自己之前学的也是懵懵懂懂&#xff0c;于是专门花了一个小时去了解了如何配置代理跨域&#xff0c;然后在此记录一下&#xff0c;方便自己以后查阅。 一、 常用…

RBF神经网络原理和matlab实现

1.案例背景 1.1 RBF神经网络概述 径向基函数(Radical Basis Function,RBF)是多维空间插值的传统技术,由Powell于1985年提出。1988年, Broomhead和 Lowe根据生物神经元具有局部响应这一特点,将 RBF引入神经网络设计中,产生了RBF神经网络。1989 年&#xff0c;Jackson论证了…

应用层协议——https

文章目录 1. HTTPS 是什么2. 什么是"加密"3. 常见的加密方式4. 数据摘要 && 数字签名5. HTTPS 的工作过程探究5.1 方案1 - 只使用对称加密5.2 方案2 - 只使用非对称加密5.3 方案3 - 双方都使用非对称加密5.4 方案4 - 非对称加密 对称加密5.5 中间人攻击5.6 …

【深度学习】InST,Inversion-Based Style Transfer with Diffusion Models,论文

代码&#xff1a;https://github.com/zyxElsa/InST 论文&#xff1a;https://arxiv.org/abs/2211.13203 文章目录 AbstractIntroductionRelated WorkImage style transferText-to-image synthesisInversion of diffusion models MethodOverview ExperimentsComparison with Sty…

C++ 多进程学习总结

C多进程 进程间通信 消息队列 消息队列&#xff1a;提供一个种进程间发送/接收数据块&#xff08;常为结构体数据&#xff09;的方法。 函数接口 ftok()&#xff1a;获取消息队列键值msgget()&#xff1a;创建和访问消息队列msgsnd()&#xff1a;向消息队列发送数据msgrcv…

VSCode配置之C++ SQLite3极简配置方案

背景 最近在学习《深入应用C11: 代码优化与工程级应用》&#xff0c;其中第13章说到SQLite库&#xff0c;查询网上诸多教程&#xff0c;发现比较容易出现bug且配置较为麻烦&#xff0c;故记录此次简化版方案&#xff0c;以供参考。 软件环境 SQLite 3.42.0 版本&#xff08;仅…

Python 进阶(五):os 模块

❤️ 博客主页&#xff1a;水滴技术 &#x1f338; 订阅专栏&#xff1a;Python 入门核心技术 &#x1f680; 支持水滴&#xff1a;点赞&#x1f44d; 收藏⭐ 留言&#x1f4ac; 文章目录 1. 文件和目录的基本操作1.1 获取当前工作目录1.2 更改当前工作目录1.3 获取目录下所有…

ffmpeg批量分割视频解决视频前几秒黑屏的问题解决

echo 请输入视频地址&#xff1a; set /p fp echo 请输入开始时间&#xff1a; set /p st echo 请输入结束时间&#xff1a; set /p et echo 请输入分片时间&#xff1a; set /p sgt echo 注意&#xff1a;循环范围参数要空格。 for /l %%i in (%st%, %sgt%, %et%) do call :aa…