【iOS】KVOKVC原理

news2024/12/23 14:02:12

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;
@end

NS_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;
@end

NS_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;
@end

NS_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;
@end

NS_ASSUME_NONNULL_END


// SXTeacher.h
#import <Foundation/Foundation.h>
#import "SXStudent.h"

NS_ASSUME_NONNULL_BEGIN

@interface SXTeacher : NSObject
@property (nonatomic, strong) SXStudent *student1;
@end

NS_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 {
    @public
    int _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.coloradmin.cn/o/791155.html

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

相关文章

Windows实现端口转发(附配置过程图文详解)

文章目录 1. 前言2. 命令提示符3. 防火墙4. netsh 命令4.1 查看已有的转发规则4.2 新增转发规则4.3 删除转发规则 5. 图解汇总6. 欢迎纠正~ 1. 前言 利用Windows端口转发&#xff0c;实现本地设备 ⬅➡ 公网主机 ⬅➡ 远端服务器 2. 命令提示符 以管理员身份打开“命令提示…

【雕爷学编程】Arduino动手做(88)---水流量传感器模块2

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

2023年深圳杯数学建模C题无人机协同避障航迹规划

2023年深圳杯数学建模 C题 无人机协同避障航迹规划 原题再现&#xff1a; 平面上A、B两个无人机站分别位于半径为500 m的障碍圆两边直径的延长线上&#xff0c;A站距离圆心1 km&#xff0c;B站距离圆心3.5 km。两架无人机分别从A、B两站同时出发&#xff0c;以恒定速率10 m/s…

Codeforces Round 888 (Div. 3)

原题链接&#xff1a;Dashboard - Codeforces Round 888 (Div. 3) - Codeforces 目录 A. Escalator Conversations B. Parity Sort C. Tiles Comeback D. Prefix Permutation Sums E. Nastya and Potions F. Lisa and the Martians A. Escalator Conversations 题意&…

【Python从入门到人工智能】14个必会的Python内置函数(7)——数据格式化处理综合应用场景 (实现程序主界面)

总觉得忍一忍就会好起来。真笨&#xff0c;人家不就是觉得你会忍一忍&#xff0c;所以才这样对你吗&#xff1f;当我们凶狠地对待这个世界的时候&#xff0c;才会发现这个世界&#xff0c;突然变得温文尔雅了。——余华《在细雨中呼喊》 &#x1f3af;作者主页&#xff1a; 追光…

第二章:Learning Deep Features for Discriminative Localization ——学习用于判别定位的深度特征

0.摘要 在这项工作中&#xff0c;我们重新审视了在[13]中提出的全局平均池化层&#xff0c;并阐明了它如何明确地使卷积神经网络&#xff08;CNN&#xff09;具有出色的定位能力&#xff0c;尽管它是在图像级别标签上进行训练的。虽然这个技术之前被提出作为一种训练规范化的手…

Docker构建Nginx镜像并部署前台应用

文章目录 1. 简介2. 准备工作3. 编写Dockerfile4. 编写nginx.conf5. 构建镜像6. 查看镜像是否构建成功7. 运行容器8. 访问Web应用9. 总结 1. 简介 Docker是一个开源的容器化平台&#xff0c;它可以帮助我们快速构建、发布和运行应用程序&#xff0c;实现应用程序的环境隔离和依…

全国大学生数据统计与分析竞赛2021年【本科组】-B题:用户消费行为价值分析

目录 摘 要 1 任务背景与重述 1.1 任务背景 1.2 任务重述 2 任务分析 3 数据假设 4 任务求解 4.1 任务一&#xff1a;数据预处理 4.1.1 数据清洗 4.1.2 数据集成 4.1.3 数据变换 4.2 任务二&#xff1a;对用户城市分布情况与分布情况可视化分析 4.2.1 城市分布情况可视化分析 4…

PWM定时器精准定时实现led闪烁(S3C2440裸机开发)

文章目录 前言一、PWM定时器原理二、使用步骤总结 前言 上期和大家分享了使用PWM定时器输出周期方波驱动蜂鸣器&#xff0c;那么本期分享的内容是使用PWM定时器实现定时器的功能&#xff0c;有了上期的基础&#xff0c;这期分享的内容大家理解起来应该非常easy&#xff0c;接下…

Psyco模块能优化Python的运行速度吗

目录 什么是Psyco模块 Psyco模块有什么作用 什么时候用Psyco模块 Psyco模块能优化Python的运行速度吗 总结 什么是Psyco模块 Psyco是一个用于优化Python代码的第三方模块。它的目标是通过即时编译&#xff08;Just-In-Time Compilation&#xff09;技术来提高Python程序的…

如何把pdf转成word文档格式?分享三个好用方法!

PDF文件和Word文档是我们日常生活和工作中最常见的两种文档格式。尽管PDF以其稳定的格式和出色的跨平台兼容性受到人们的喜爱&#xff0c;但在文本编辑方面&#xff0c;Word文档更具有灵活性。因此&#xff0c;将PDF转换为Word文档的需求在我们日常生活中非常常见。这篇文章将为…

微信小程序入门教程||微信小程序 小程序宿主环境||微信小程序 小程序协同工作和发布

微信小程序 小程序宿主环境 小程序宿主环境 我们称微信客户端给小程序所提供的环境为宿主环境。小程序借助宿主环境提供的能力&#xff0c;可以完成许多普通网页无法完成的功能。 上一章中我们把小程序涉及到的文件类型阐述了一遍&#xff0c;我们结合 QuickStart 这个项目来…

Access设置或取消密码

数据库密码”解决办法 Access设置或取消密码提示““必须保持数据库打开才可专门用于设置或删除数据库密码”” 解决方法&#xff1a; 按照提示的描述&#xff0c;需要“打开转属项”。 其实&#xff0c;这里是指需要以独占方式打开文件&#xff0c;打开方式如下&#xff1a…

常用协议的相关

远程登录协议&#xff1a; 23端口&#xff0c;TCP连接。 C/S方式。 文本传输协议FTP&#xff1a; C/S方式。 建立两条TCP连接&#xff0c;一条用于传送控制信息&#xff0c;一条用于传送文件内容。 FTP的控制连接采用了Telent协议。主要是用来进行简单的身份认证系统&…

中文人物关系知识图谱(含码源):中文人物关系图谱构建、数据回标、基于远程监督人物关系抽取、知识问答等应用.

项目设计集合&#xff08;人工智能方向&#xff09;&#xff1a;助力新人快速实战掌握技能、自主完成项目设计升级&#xff0c;提升自身的硬实力&#xff08;不仅限NLP、知识图谱、计算机视觉等领域&#xff09;&#xff1a;汇总有意义的项目设计集合&#xff0c;助力新人快速实…

opencv hand openpose

使用opencv c 来调用caffemodel 使用opencv 得dnn 模块调用 caffemodel得程序&#xff0c;图片自己输入就行&#xff0c;不做过多得解释&#xff0c;看代码清单。 定义手指关节点 const int POSE_PAIRS[20][2] { {0,1}, {1,2}, {2,3}, {3,4}, // thumb {0,5}, {5,6}, {6,7}…

前端技术搭建(动态图片)拖拽拼图!!(内含实现原理)

文章目录 前端技术搭建&#xff08;动态图片&#xff09;拖拽拼图(内含实现原理)导言功能介绍效果演示链接&#xff08;觉得不错的&#xff0c;请一键三连嘤嘤嘤&#xff09;项目目录页面搭建css样式设置工具函数游戏实现逻辑 开源地址总结 前端技术搭建&#xff08;动态图片&a…

热门洗地机评测|追觅VS希亦VS米博洗地机,哪款更值得入手?

智能科技的发展越来越方便人们的生活&#xff0c;特别是现今人们生活水平不断提高&#xff0c;房子越住越大&#xff0c;需要顾及的房屋卫生打扫面积也越来越广。而单是通过人工去拖扫不仅很累还很浪费时间。于是洗地机的出现让很多深陷家务劳动的朋友得以解脱。因为很多洗地机…

人工智能-Dlib+Python实现人脸识别(人脸检测以及68点特征提取)

Dlib是一个现代的C ++工具包,包含机器学习算法和工具,用于在C ++中创建复杂的软件来解决实际问题。它广泛应用于工业界和学术界,包括机器人,嵌入式设备,移动电话和大型高性能计算环境。Dlib的开源许可 允许您在任何应用程序中免费使用它 Dlib可以使用pip install来安装或…

基于开源IM即时通讯框架MobileIMSDK:RainbowChat v9.0版已发布

关于MobileIMSDK MobileIMSDK 是一套专门为移动端开发的开源IM即时通讯框架&#xff0c;超轻量级、高度提炼&#xff0c;一套API优雅支持UDP 、TCP 、WebSocket 三种协议&#xff0c;支持iOS、Android、H5、标准Java平台&#xff0c;服务端基于Netty编写。 工程开源地址是&am…