【iOS】KVO

文章目录

  • 前言
  • 一、KVO使用
    • 1.基本使用
    • 2.context使用
    • 3.移除KVO通知的必要性
    • 4.KVO观察可变数组
  • 二、代码调试探索
    • 1.KVO对属性观察
    • 2.中间类
    • 3.中间类的方法
    • 3.dealloc中移除观察者后,isa指向是谁,以及中间类是否会销毁?
    • 总结
  • 三、KVO本质
    • GNUStep窥探KVO源码
      • 重写setter方法
      • 重写class方法
      • 重写delloc方法
      • 重写KVC方法
      • 成员变量使用KVC触发KVO
  • 总结


前言

KVO的全称 Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变

KVO是一种机制,它允许将其他对象的指定属性的更改通知给对象

在iOS官方文档中有这么一句话:
理解KVO之前,必须先理解KVC(即KVO是基于KVC基础之上)

In order to understand key-value observing, you must first understand key-value coding.
KVC是键值编码,在对象创建完成后,可以动态的给对象属性赋值,而KVO是键值观察,提供了一种监听机制,当指定的对象的属性被修改后,则对象会收到通知,所以可以看出KVO是基于KVC的基础上对属性动态变化的监听

我们知道NSNotificatioCenter也是一种监听方式,那么KVONSNotificatioCenter有什么区别呢?

  • 相同点:
    1、两者的实现原理都是观察者模式,都是用于监听

2、都能实现一对多的操作

  • 不同点:
    1、KVO监听对象属性的变化,同时只能通过NSString来查找属性名,较容易出错

2、NSNotification的发送监听(post)的操作我们可以控制,kvo由系统控制。

3、KVO可以记录新旧值变化

一、KVO使用

1.基本使用

KVO的基本使用分为三步

  • 注册观察addObserver:forKeyPath:options:context
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
  • 实现KVO回调observeValueForKeyPath:ofObject:change:context
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",change);
    }
}
  • 移除观察者removeObserver:forKeyPath:context
[self.person removeObserver:self forKeyPath:@"nick" context:NULL];

2.context使用

我们注意到这些方法中都有参数context,我们来讲解一下

context 参数的主要作用是为 KVO 回调提供一个标识符或标记,这有助于区分同一属性上的不同观察者或在多个地方注册的同一个观察者

在官方文档中,针对参数context有如下说明:
在这里插入图片描述
通俗的讲,context上下文主要是用于区分不同对象的同名属性,从而在KVO回调方法中避免使用字符串进行区分,而是直接使用context进行区分,可以大大提升性能,以及代码的可读性

因此我们可以知道,context常用于标识,从而区分
不同对象的同名属性

context使用总结

  • 不使用context,使用keyPath区分通知来源
//context的类型是 nullable void *,应该是NULL,而不是nil
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
  • 使用context区分通知来源
//定义context
static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;

//注册观察者
[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    
    
//KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == PersonNickContext) {
        NSLog(@"%@",change);
    }else if (context == PersonNameContext){
        NSLog(@"%@",change);
    }
}

3.移除KVO通知的必要性

首先我们需要理解一下观察者与被观察者,例如下面这段代码:

[self.person addObserver:self
                  forKeyPath:@"name"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];

观察者将观察 Person 类的 name 属性的变化。在这个例子中,我们将使用 ViewController 作为观察者

在官方文档中,针对KVO的移除有以下几点说明
在这里插入图片描述

删除观察者时,请记住以下几点:

  • 要求被移除为观察者(如果尚未注册为观察者)会导致NSRangeException。您可以对removeObserver:forKeyPath:context:进行一次调用,以对应对addObserver:forKeyPath:options:context:的调用,或者,如果在您的应用中不可行,则将removeObserver:forKeyPath:context:调用在try / catch块内处理潜在的异常。

  • 释放后,观察者不会自动将其自身移除。被观察对象继续发送通知,而忽略了观察者的状态。但是,与发送到已释放对象的任何其他消息一样,更改通知会触发内存访问异常。因此,您可以确保观察者在从内存中消失之前将自己删除。

  • 该协议无法询问对象是观察者还是被观察者。构造代码以避免发布相关的错误。一种典型的模式是在观察者初始化期间(例如,在init或viewDidLoad中)注册为观察者,并在释放过程中(通常在dealloc中)注销,以确保成对和有序地添加和删除消息,并确保观察者在注册之前被取消注册,从内存中释放出来。

KVO注册观察者 和移除观察者是需要成对出现的,如果只注册,不移除,会出现类似野指针的崩溃,如下图所示
在这里插入图片描述

崩溃的原因是,由于第一次注册KVO观察者后没有移除,再次进入界面,会导致第二次注册KVO观察者,导致KVO观察的重复注册,而且第一次的通知对象还在内存中,没有进行释放,此时接收到属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,所以导致了类似野指针的崩溃,即一直保持着一个野通知,且一直在监听

其实简单来讲就是可能当我们推出视图控制器时,视图控制器已经被销毁,同时我们的观察者是视图控制器,但是我们的视图控制器仍然是观察者,并没有被移除,因此当我们后续继续通过被观察者通知观察者时,就会出现观察者时已经被销毁的视图控制器,从而出现访问野指针的情况导致崩溃

4.KVO观察可变数组

KVO是基于KVC基础之上的,所以可变数组如果直接添加数据,是不会调用setter方法的,所有对可变数组的KVO观察下面这种方式不生效的,即直接通过[self.person.dateArray addObject:@“1”];向数组添加元素,是不会触发kvo通知回调的

在KVC官方文档中,针对可变数组的集合类型,有如下说明,即访问集合对象需要需要通过mutableArrayValueForKey方法,这样才能将元素添加到可变数组

    [_t addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
     [_t.array addObject:@1];

这样不会出发通知,即使数组元素改变

我们应该使用mutableArrayValueForKey方法

    [_t addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew | 
    [[self.t mutableArrayValueForKey:@"array"] addObject:@"1"];

二、代码调试探索

1.KVO对属性观察

现在有一个属性与成员变量,分别注册KVO并且直接修改他们的值
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
发现只有age属性发生了变化
在这里插入图片描述

结论:
KVO只观察属性,不直接观察成员变量,这是因为setter方法的原因,但是使用KVC修改成员变量可以触发KVO

KVO 通常只能观察通过属性的 setter 方法修改的属性。这是因为当您为某个属性添加观察者时,Objective-C
运行时会动态创建该属性的一个特殊子类,并在这个子类中重写 setter 方法来插入属性变化通知的代码。由于直接修改成员变量不会触发
setter 方法
,因此不会产生 KVO 通知。

2.中间类

我们刚才提到了在运行时会创建一个中间类,接下来我们讲解一下这个中间类

根据官方文档所述,在注册KVO观察者后,观察对象的isa指针指向会发生改变

在注册观察者前后,对象的isa指针发生了变化
在这里插入图片描述

综上所述,在注册观察者后,实例对象的isa指针指向由kunkun类变为了NSKVONotifying_kunkun中间类,即实例对象的isa指针指向发生了变化
在这里插入图片描述

3.中间类的方法

既然生成了一个中间类,那么我们来查看一下这个中间类中有什么方法

#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

//********调用********
[self printClassAllMethod:objc_getClass("NSKVONotifying_kunkun")];

输出:
在这里插入图片描述

那么我们的父类也有一个setAge方法,那么这里的这个方法是继承还是重写呢?
我们接下来打印父类的方法列表看一下
在这里插入图片描述
从这里说明继承的方法不会在子类中显示,所以NSKVONotifying_kunkun重写了set方法

综上所述,有如下结论:

  • NSKVONotifying_kunkun中间类重写了父类kunkunsetAge方法
  • NSKVONotifying_kunkun中间类重写了基类NSObjectclass 、 dealloc 、 _isKVOA方法
    其中dealloc是释放方法
    _isKVOA判断当前是否是kvo

我们这里再来设计一个函数来验证中间类与类的关系

创建一个函数来遍历所有已注册的类,并检查它们是否是指定类的子类。

void PrintSubclassesOfClass(Class parentClass) {
    int numClasses = objc_getClassList(NULL, 0);
    Class *classes = NULL;

    classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
    numClasses = objc_getClassList(classes, numClasses);

    for (int i = 0; i < numClasses; i++) {
        Class cls = classes[i];
        Class superClass = class_getSuperclass(cls);
        
        while (superClass) {
            if (superClass == parentClass) {
                NSLog(@"%@ is a subclass of %@", NSStringFromClass(cls), NSStringFromClass(parentClass));
                break;
            }
            superClass = class_getSuperclass(superClass);
        }
    }

    free(classes);
}

// 调用PrintSubclassesOfClass([_t class]);

在这里插入图片描述

由此发现中间类是类的子类,用到了isa swizzling技术

3.dealloc中移除观察者后,isa指向是谁,以及中间类是否会销毁?

    [_t addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [_t removeObserver:self forKeyPath:@"age"];

这两段代码执行后分别打印其isa指向
在这里插入图片描述
由此可见移除观察者后isa又变回了原来的指向

同时我们再次调用子类查找函数

    [_t addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [_t removeObserver:self forKeyPath:@"age"];
    PrintSubclassesOfClass([_t class]);

输出:
在这里插入图片描述
说明中间类仍然存在没有被销毁

这里可能是考虑到重用的技术,后面再次注册观察者就不用重复生成中间类

总结

综上所述,关于中间类,有如下说明:

  • 实例对象isa的指向在注册KVO观察者之后,由原有类更改为指向中间类
  • 中间类重写了观察属性的setter方法、class、dealloc、_isKVOA方法
  • dealloc方法中,移除KVO观察者之后,实例对象isa指向由中间类更改为原有类
  • 中间类从创建后,就一直存在内存中,不会被销毁

由此我们可以得到如下关系图
在这里插入图片描述

三、KVO本质

在前面铺垫了那么多,我们现在来讲讲KVO的实现流程

KVO的本质是改变setter方法的调用

首先我们知道了中间类重写了setter方法,我们来打印一下重写后的方法的IMP,也就是方法实际上会调用哪一个函数
在这里插入图片描述
当修改instance对象的属性时,会调用Foundation_NSSetXXXValueAndNotify函数

Foundation框架中还有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函数。
在这里插入图片描述

GNUStep窥探KVO源码

由于KVO的实现没有开源,因此我们无法查看KVO的源码

GNUStep是一个成熟的框架,适用于高级GUI桌面应用程序和服务器应用程序,它将Cocoa Objective-C软件库,以自由软件方式重新实现,能够运行在Linux和windows操作系统上。

GNUStepFoundation与apple的API相同,虽然具体实现可能不一样,但仍旧有借鉴意义。

重写setter方法

GNUStep有一个模板类叫做GSKVOSetter,针对不同的数据类型,都有一个不同的setter方法实现,列举其中一个方法:

- (void) setterChar: (unsigned char)val
{
  NSString  *key; // 定义一个用来存储属性名称的字符串
  Class     c = [self class]; // 获取当前对象的类

  // 定义一个函数指针,用来存储原始的 setter 方法的实现
  void      (*imp)(id,SEL,unsigned char);

  // 通过类和当前方法的选择器(_cmd),获取这个方法的原始实现,并转换为适当的函数指针类型
  imp = (void (*)(id,SEL,unsigned char))[c instanceMethodForSelector: _cmd];

  // 通过 _cmd 选择器获取与之关联的属性名,通常通过移除 set 前缀和小写化首字母实现
  key = newKey(_cmd); // 这个 newKey 函数的实现没有给出,假设它能从 setter 名生成属性名

  // 检查这个类是否为 key 提供自动 KVO 通知
  // 这个检查是由 automaticallyNotifiesObserversForKey: 方法进行,该方法默认返回 YES
  if ([c automaticallyNotifiesObserversForKey: key] == YES) // 通常总是返回 YES,除非在子类中被重写
  {
      [self willChangeValueForKey: key]; // 在改变值之前手动通知 KVO 系统属性即将变更
      (*imp)(self, _cmd, val); // 调用原始的 setter 方法实现来更新属性值
      [self didChangeValueForKey: key]; // 在改变值之后手动通知 KVO 系统属性已经变更
  }
  else
  {
      // 如果类表示不自动通知,则直接调用原始实现,不发送 KVO 通知
      (*imp)(self, _cmd, val);
  }
  RELEASE(key); // 释放之前为 key 分配的内存(这个假设 key 是动态分配的,但代码中没有显示这部分)
}

由此我们可以知道重写后的setter方法的主要步骤

      [self willChangeValueForKey: key]; // 在改变值之前手动通知 KVO 系统属性即将变更
      (*imp)(self, _cmd, val); // 调用原始的 setter 方法实现来更新属性值
      [self didChangeValueForKey: key]; // 在改变值之后手动通知 KVO 系统属性已经变更
  • 先调用willChangeValueForKey方法,
  • 再调用父类原来的setter方法
  • 最后调用didChangeValueForKey,其内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:);

我们用代码来验证一下调用顺序

- (void)setAge:(int)age {
        _age = age; // 直接赋值操作,确保使用下划线来访问实例变量,避免递归调用setter
        NSLog(@"调用成功:已将 age 设置为 %d", _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");
}

在这里插入图片描述
符合我们上面所说的流程,同时在didChangeValueForKey方法中我们调用了observeValueForKeyPath:ofObject:change:context:,由此我们可以推测一下observeValueForKeyPath:ofObject:change:context:的实现代码
在这里插入图片描述

重写class方法

由于我们不想中间类暴露给用户,因此我们的程序同时重写了中间类的class方法

- (Class) class
{
  return class_getSuperclass(object_getClass(self));
}

由此我们class方法返回的就是原来的实例对象所属的类,而非中间类

重写delloc方法

- (void) dealloc
{
  // Turn off KVO for self ... then call the real dealloc implementation.
  [self setObservationInfo: nil];
  object_setClass(self, [self class]);
  [self dealloc];
  GSNOSUPERDEALLOC;
}

- (void) dealloc对象释放后,移除KVO数据,将对象重新指向原始类

重写KVC方法

- (void) setValue: (id)anObject forKey: (NSString*)aKey这是KVC中的方法,但是在GNUStep中也重写了这个方法

- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
  Class     c = [self class];
  void      (*imp)(id,SEL,id,id);

  imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];

  if ([[self class] automaticallyNotifiesObserversForKey: aKey])
    {
      [self willChangeValueForKey: aKey];
      imp(self,_cmd,anObject,aKey);
      [self didChangeValueForKey: aKey];
    }
  else
    {
      imp(self,_cmd,anObject,aKey);
    }
}

这与我们上面讲到的重写后的setter方法类似,实现在原始类KVC调用前后添加[self willChangeValueForKey: aKey][self didChangeValueForKey: aKey],而这两个方法是触发KVO通知的关键。
所以说KVO是基于KVC的,而KVC正是KVO触发的入口

成员变量使用KVC触发KVO

由此如果我们直接修改成员变量不会触发KVO,但是如果通过KVC修改成员变量就会触发KVO

在这里插入图片描述

[_t setValue:@5 forKey:@"height"];
    NSLog(@"@%d", _t->height);

在这里插入图片描述

总结

  • KVC是KVO的入口,网上许多人说成员变量无法被KVO观察,其实是可以的,只是需要调用KVC,但是面试时一般都会说KVO只能用来观察属性
  • KVO的实现主要就是通过isa swizzling技术交换isa指针,在运行时生成中间类,在中间类中重写setter方法从而通知触发KVO监听函数。
  • 重写后的setter方法调用顺序主要为willChangeValueForKey->setter方法->didChangeValueForKey
  • 同时移除观察者后中间类会一直存在等待重用
  • 参考博客
    iOS底层原理总结 - 探寻KVO本质
    KVO源码浅析
    iOS-底层原理 23:KVO 底层原理

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

虚拟化技术 使用Vsphere Client管理ESXi服务器系统

使用Vsphere Client管理ESXi服务器系统 一、实验目的与要求 1.掌握使用vSphere Client管理ESXi主机 2.掌握将CentOS的安装介质ISO上传到ESXi存储 3.掌握在VMware ESXi中创建虚拟机 4.掌握在所创建的虚拟机中安装CentOS6.5操作系统 5.掌握给CentOS6.5安装VMware Tools 6.掌…

HCIP的学习(11)

OSPF的LSA详解 LSA头部信息 ​ [r2]display ospf lsdb router 1.1.1.1----查看OSPF某一条LSA的详细信息&#xff0c;类型以及LS ID参数。 链路状态老化时间 指一条LSA的老化时间&#xff0c;即存在了多长时间。当一条LSA被始发路由器产生时&#xff0c;该参数值被设定为0之后…

RK3568 学习笔记 : u-boot 千兆网络无法 ping 通PC问题的解决方法二

参考 RK3568 学习笔记 : u-boot 千兆网络无法 ping 通PC问题的解决 前言 rk3568 rockchip 提供的 u-boot&#xff0c;默认的设备树需要读取 单独分区 resouce.img 镜像中的 设备树文件&#xff0c;也就是 Linux 内核的设备树 dtb 文件&#xff0c;gmac 网络才能正常的 ping 通…

知识图谱和大语言模型的共存之道

导读 知识图谱和大型语言模型都是用来表示和处理知识的手段。大模型补足了理解语言的能力&#xff0c;知识图谱则丰富了表示知识的方式&#xff0c;两者的深度结合必将为人工智能提供更为全面、可靠、可控的知识处理方法。在这一背景下&#xff0c;OpenKG组织新KG视点系列文章—…

Microsoft Remote Desktop Beta for Mac:远程办公桌面连接工具

Microsoft Remote Desktop Beta for Mac不仅是一款远程桌面连接工具&#xff0c;更是开启远程办公新篇章的利器。 它让Mac用户能够轻松访问和操作远程Windows计算机&#xff0c;实现跨平台办公的无缝衔接。无论是在家中、咖啡店还是旅途中&#xff0c;只要有网络连接&#xff0…

如何使用DEEPL免费翻译PDF

如何使用DEEPL免费翻译PDF 安装DEEPL取消PDF限制 安装DEEPL 安装教程比较多&#xff0c;这里不重复。 把英文pdf拖进去&#xff0c;点翻译&#xff0c;在下面的框中有已经翻译完毕的文档。 但是存在两个问题 问题1&#xff1a;这些文档是加密的。 问题2&#xff1a;带有DeepL标…

Golang | Leetcode Golang题解之第70题爬楼梯

题目&#xff1a; 题解&#xff1a; func climbStairs(n int) int {sqrt5 : math.Sqrt(5)pow1 : math.Pow((1sqrt5)/2, float64(n1))pow2 : math.Pow((1-sqrt5)/2, float64(n1))return int(math.Round((pow1 - pow2) / sqrt5)) }

Java基于Spring Boot框架的课程管理系统(附源码,说明文档)

博主介绍&#xff1a;✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3…

【微服务】分布式事务(通过Seata解决分布式事务问题)

分布式事务 分布式事务Seata微服务集成SeataXA模式XA模式使用 AT模式AT模式实现 分布式事务 在分布式系统中&#xff0c;如果一个业务需要多个服务合作完成&#xff0c;而且每一个服务都有事务&#xff0c;多个事务必须同时成功或失败&#xff0c;这样的事务就是分布式事务&am…

解决Maven本地仓库存在依赖包还需要远程下载的问题

背景 公司有自己maven私服&#xff0c;正在在私服可以使用的情况&#xff0c;打包是没问题的。但是这次是由于公司大楼整体因电路检修而停电&#xff0c;所有服务器关机&#xff0c;包括maven私服服务器。然后当天确有一个包需要打&#xff0c;这个时候发现死活打不了&#xf…

自然语言处理(NLP)原理、用法、案例、注意事项

自然语言处理&#xff08;Natural Language Processing&#xff0c;简称NLP&#xff09;是人工智能&#xff08;Artificial Intelligence&#xff0c;简称AI&#xff09;领域的一个重要分支&#xff0c;旨在让计算机能够理解、理解和生成人类语言。 NLP的原理是基于统计建模和机…

list 的模拟实现

目录 1. list 的实现框架 2. push_back 3. 迭代器 4. constructor 4.1. default 4.2. fill 4.3. range 4.4. initializer list 5. insert 6. erase 7. clear 和 destructor 8. copy constructor 9. operator 10. const_iterator 10.1. 普通人的处理方案 10.2. …

物联网小demo

机智云生成代码 具体参考之前的文章 初始化 ADC用来使用光敏电阻 连续采样开启 采样的周期调高 定时器 定时器1用来实现延时 为了只用温湿度模块DHT11 定时器4用来和51进行交互 实现定时的发送和检测心跳信号 IIC 用来使用oled屏幕 USART 串口1和串口2是机智云自己…

Linux —— 信号初识

Linux —— 信号初识 什么是信号测试几个信号signal函数函数原型参数说明返回值注意事项示例 后台程序前台转后台检测输入中断向量表 我们今天来继续学习Linux的内容&#xff0c;今天我们要了解的是Linux操作系统中的信号&#xff1a; 什么是信号 信号是操作系统内核与进程之…

Hash Function(fft)

链接&#xff1a;H-Hash Function_2024牛客五一集训派对day4 (nowcoder.com) 题意&#xff1a;给定一个序列&#xff0c;求使得任意两数的hash值不同的最小模数&#xff1b; 分析&#xff1a;ab(mod seed) |a-b|%seed0; 也就是说seed不能是任意两数差的因子。 如果暴力求解…

【大麦小米学量化】使用Python读写通达信自选股(含代码转换及完整源代码),想要通过通达信自选股实现量化自动关联交易的有福了

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、通达信自选股文件所在位置二、通达信自选股文件数据结构三、使用Python读写通达信自选股文件&#xff08;附完整源代码&#xff09;1. 切换目录路径2. 将li…

4月30日重庆某厂酸碱管道整改工作汇报-智渍洁

时间:2024.4.30 地点:******老厂酸碱管道整改 施工人员:王成、汪勇、郭建华 事项:老厂酸碱管道更换 完成进度100%酸碱管道支架以添加完成&#xff01;碱管道保温已完成&#xff01; 1吨桶未完成2主水管漏水未处理&#xff0c;3酸 水泵需更换全新4室内少许添加活未完成。 4月30日…

精析React与Vue架构异同及React核心技术——涵盖JSX、组件、Props、State、生命周期与16.8版后Hooks深化解析

React&#xff0c;Facebook开源的JavaScript库&#xff0c;用于构建高性能用户界面。通过组件化开发&#xff0c;它使UI的构建、维护变得简单高效。利用虚拟DOM实现快速渲染更新&#xff0c;适用于单页应用、移动应用&#xff08;React Native&#xff09;。React极大推动了现代…

2-qt之信号与槽-简单实例讲解

前言、因实践课程讲解需求&#xff0c;简单介绍下qt的信号与槽。 一、了解信号与槽 怎样使用信号与槽&#xff1f; 概览 还记得 X-Window 上老旧的回调函数系统吗&#xff1f;通常它不是类型安全的并且很复杂。&#xff08;使用&#xff09;它&#xff08;会&#xff09;有很多…

Redis-分片机制

概述 业务需要&#xff1a;由于单台redis内存容量是有限的&#xff0c;无法实现海量的数据实现缓存存储 概念&#xff1a;由多个redis节点协助工作的机制就是redis的分片机制 作用&#xff1a;为了实现redis扩容 特点&#xff1a;分片机制把该机制中包含的多台redis缓存服务…
最新文章