【iOS】KVO

news2024/9/27 7:26:54

文章目录

  • 前言
  • 一、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.coloradmin.cn/o/1644291.html

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

相关文章

Rust语言系统编程实战(小北学习笔记)

前言 进入大学以来&#xff08;计算机应用技术——大数据方向&#xff09;&#xff0c;就像很多程序猿&#x1f412;一样&#xff0c;小北开始每学期学习一种新的编程语言。通过学习另一个编程语言&#xff0c;可以了解很多规范和规则&#xff0c;并得到了一些想法&#xff0c;…

【Linux】目录和文件相关的命令,补充:centos7系统目录结构

【Linux】Linux操作系统的设计理念之一就是“一切皆文件”&#xff08;Everything is a file&#xff09;&#xff0c;即将设备、文件等都当作“文件”处理。 “文件”主要类型有&#xff1a;目录&#xff08;即文件夹&#xff09;&#xff0c;链接文档&#xff08;即快捷方式…

物联网小demo

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

第二代增强-创建采购申请时的增强

文章目录 第二代增强-创建采购申请时的增强业务要求实现过程创建项目编写代码激活增强 维护消息类运行效果断点关键点另一种形式的错误提示-出口函数EXIT_SAPLMEREQ_005运行结果 第二代增强-创建采购申请时的增强 业务要求 实现过程 创建项目 编写代码 "AFNAM&#xff0c…

Flutter笔记:Widgets Easier组件库(11)- 使用提示吐丝

Flutter笔记 Widgets Easier组件库&#xff08;11&#xff09;使用提示吐丝 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this …

C#核心之面向对象-多态

面向对象-多态 文章目录 1、Vob1、多态的概念2、多态的实现 2、抽象类和抽象方法1、抽象类2、抽象方法 3、接口1、接口的概念2、接口的声明3、接口的使用4、接口可以继承接口5、显示实现接口总结思考 电脑使用USB接口读取数据 4、密封方法 1、Vob 1、多态的概念 多态&#xf…

libmodbus使用

安装可以看这个博客&#xff1a; https://blog.csdn.net/hanhui22/article/details/105786762 它的安装可以&#xff0c;但是编译测试看不太懂&#xff0c;我没跟着它的编译&#xff0c;完了后把/lib下的 放到开发板的/usr/lib下 编写代码: #include <stdio.h> #inclu…

研究论文的蓝图:精通论文大纲的编写技巧

研究论文大纲是一个补充文件&#xff0c;描述了按计划顺序纳入论文的所有主题&#xff0c;通常按段落分割。正常的研究论文大纲包括额外的细节&#xff0c;例如子主题和证据来源&#xff0c;以帮助作者保持结构。本文讨论了研究论文大纲的内容以及如何撰写。 研究论文大纲的含…

【AI】指定python3.10安装Jupyter Lab

家里电脑 13900K, bash 不识别pythoncmd可以,但是cmd似乎默认是python2.7这个是webrtc构建需要的.python3 则可以识别到但是版本是python3.12*多个版本如何通过制定的python3.10 的pip来安装软件,例如Jupyter Lab安装3.10 C:\Users\zhangbin\AppData\Roaming\Microsoft\Windo…

中国各地级市城投债详细数据(2006年-2023年2月)

01、数据简介 城投债又称为准市政债&#xff0c;发行主体是地方ZF投资平台&#xff0c;公开发行企业债和中期票据&#xff0c;其业主一般是地方基础设施建设&#xff0c;或者公益性项目主体&#xff0c;参与债券发行环节的当地ZF发债。 数据整理中国各地级市的城投债详细数据…

Inflate动态Huffman解压缩

上个已经实现GZIP压缩文件格式的Inflate静态Huffman解压&#xff0c;这个实现Inflate的无压缩输出和动态Huffman解压。 Java语言实现&#xff0c;Eclipse下编写。 范式Huffman解码实现&#xff0c;输入huffman编码&#xff0c;输出原始数据 // 范式huffman解码static class C…

推荐一个开源的MES系统

软件介绍 HM-MES是一款旨在帮助工厂实现生产计划、工艺管理和质量控制的工业生产管理软件。该软件基于Java Web技术和MySql数据库开发&#xff0c;拥有简洁、易用、安全和稳定等特点&#xff0c;适用于广泛的生产管理场景。 功能描述 1.产品和原材料双向溯源&#xff0c;支持二…

练习题(2024/5/3)

1对称二叉树 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&#xff1a; 树中…

meshlab: pymeshlab计算两个模型的布尔交集(mesh boolean intersection)

一、关于环境 请参考&#xff1a;pymeshlab遍历文件夹中模型、缩放并导出指定格式-CSDN博客 二、关于代码 本文所给出代码仅为参考&#xff0c;禁止转载和引用&#xff0c;仅供个人学习。 本案例以两个圆环为例。 左侧为两个圆环&#xff0c;右上是重叠&#xff0c;右下是圆…

斯坦福开源端侧大模型Octopus v2,2B参数量可在移动端运行,性能超越GPT-4,准确率超Llama7B

前言 斯坦福大学研究人员近日推出了开源端侧大模型Octopus v2&#xff0c;引起了广泛关注。Octopus v2拥有20亿参数量&#xff0c;可以在智能手机、车载系统等终端设备上高效运行&#xff0c;在准确性和推理速度方面都超越了GPT-4。 Huggingface模型下载&#xff1a;https://h…

力扣每日一题113:路径总和||

题目 中等 给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSu…

【Java】基本程序设计结构(二)

前言&#xff1a;上一篇我们详细介绍了Java基本程序设计结构中前半部分&#xff0c;一个简单的Java应用&#xff0c;注释&#xff0c;数据类型&#xff0c;变量与常量&#xff0c;运算符&#xff0c;字符串。包括本篇将延续上篇内容介绍后续内容&#xff0c;包括输入输出&#…

PC端Discord设置代理2022

开始使用 方法非常简单&#xff0c;在此下载release.zip并解压version.dll https://github.com/aiqinxuancai/discord-proxy/releases​github.com/aiqinxuancai/discord-proxy/releases 将version.dll放在Discord.exe所在目录&#xff08;如Discord有更新&#xff0c;则可能…

华为二层交换机与路由器连通上网实验

华为二层交换机与路由器连通上网实验 二层交换机是一种网络设备&#xff0c;用于在局域网&#xff08;LAN&#xff09;中转发数据帧。它工作在OSI模型的第二层&#xff0c;即数据链路层。二层交换机通过学习和维护MAC地址表&#xff0c;实现了数据的快速转发和广播域的隔离。 实…

Kannala-Brandt 鱼眼相机模型

最近在学习 ORB-SLAM3 的源代码&#xff0c;并模仿、重构了相机模型的实现 在学习的过程中发现针孔相机 (Pinhole) 与鱼眼相机 (Fisheye) 都有畸变参数&#xff0c;但是鱼眼相机无法使用 cv::undistort 函数去畸变 在对鱼眼相机的深度归一化平面进行可视化后&#xff0c;发现…