一文读懂iOS中的Crash捕获、分析以及防治

news2025/1/19 6:17:29

Crash系统性总结

  • Crash捕获与分析
    • Crash收集
    • 符号化分析
  • Crash类别以及解法分析
    • 子线程访问UI而导致的崩溃
    • unrecognized selector send to instance xxx
    • KVO crash
    • KVC造成的crash
    • NSTimer导致的Crash
    • 野指针
    • Watch Dog超时造成的crash
    • 其他crash待补充
  • 参考文章:

对于iOS端开发,定位和解决Crash毕竟两个流程,首先是根据线索来分析和定位问题,得到一个大概的猜想,之后按照自己的猜想去提供外部条件,来尝试复现问题,如果问题能够成功复现并复原与线程问题相似的堆栈现场,则基本完成了90%的工作,剩下的10%才是修复此问题。对于crash比例极低的,例如没有版本相关性的,对我们的应用影响极小的,我们可以通过去做AB实验尝试去修复。

大家先思考下以下问题然后阅读文章找到答案

  1. bad_access 的排查途径有哪些 ?
  2. 什么情况下会产生 bad_access ?
  3. 不同的bad_access有什么方案可以完美解决?
    在这里插入图片描述

Crash捕获与分析

Crash收集

收集方式:

  • 利用Xcode获取
    • 将iOS设备连接到Mac电脑。
    • 打开Xcode,选择顶部菜单栏的“Window”。
    • 选中“Organizer”,然后选择“Crashes”标签。
    • 在这里,你可以看到与你的APP关联的所有崩溃日志,选择APP名字以及版本等,就可以查看各种崩溃日志。
  • 友盟、bugly、Sentry(目前我公司使用的就是这个https://sentry.io/for/ios/)等获取。
  • 通过iOS SDK中提供的线程的函数 NSSetUncaughtExceptionHandler用来做异常处理,利用NSSetUncaughtExceptionHandler
    ,当程序退出的时候,可以先进行处理,然后做一些自定义的动作,并通知开发者。(例如:我们把崩溃存在沙盒,等下次用户打开应用的时候,把crash数据上传到我们的服务器)

下面介绍如何自己手动的获取日志(也就是利用NSSetUncaughtExceptionHandler自己实现):

MyUncaughtExceptionHandler.h文件
在这里插入图片描述
MyUncaughtExceptionHandler.m文件
在这里插入图片描述

在这里插入图片描述

AppDelegate.m

在appledelegate导入头文件加上一个异常捕获监听,用来处理程序崩溃时的回调动作 在这里也要判断一下之前有没有崩溃日志 如果有发送给服务器 。

在这里插入图片描述

上方代码就已经 可以获取到 carsh日志了。我们现在来尝试一下,做一个crash代码,然后打开沙盒的log日志。
Carsh代码如下(实现一个kvc中的key为nil的crash):

在这里插入图片描述

取出沙盒的日志如下:

在这里插入图片描述
我们可以通过该表大致的得到 崩溃的原因。

符号化分析

当应用程序在IOS 设备上崩溃(例如,闪退)时,一份“Crash崩溃报告”将在该设备上创建并存储起来。崩溃报告描述了应用程序是在何种条件下崩溃的,大部分情况下包含一份当前正在运行线程的完整堆栈跟踪。
如果设备就在身边,可以连接设备,打开Xcode - Window - Organizer,在左侧面板中选择Device Logs(可以选择具体设备的Device Logs或者Library下所有设备的Device Logs),然后根据时间排序查看设备上的crash日志。这是开发、测试阶段最经常采用的方式。
如果应用程序已经提交到App Store发布,用户已经安装使用了,那么开发者可以 通过iTunes Connect (Manage Your Applications - View Details - Crash Reports)获取用户的crash日志。不过这并不是100%有效的,而且大多数开发者并不依赖于此,因为这需要用户设备同意上传相关信息。然后呢。。。

我们其实在获取到崩溃日志以后,是不知道具体哪行代码崩溃的。这个时候我们就需要获取到dsYM文件,利用dsYM符号化调用栈,找到具体代码行
长话短说就是将运行时信息转换为源码信息,符号化是一种机制,将我们在设备运行时 App 的内存地址和关联的指令信息转换为源码文件中具体文件名、方法名、行数等;可以理解为将运行时机器如何看待处理我们 App 的信息转换成我们开发者如何看待处理我们的 App(源码)。如果缺少这层转换,哪怕只有几行的代码的 App,bug 定位也变得难以进行;一般第三方的crash收集后,我们在集成SDK后,开发者需要在第三方服务的后台配置他们的应用信息,包括应用的标识符、dSYM文件的上传方式等。有些服务允许开发者通过API上传dSYM文件,而有些则要求开发者在构建应用时手动上传。
当应用发生崩溃时,第三方服务会捕获崩溃日志,并使用开发者提供的dSYM文件对日志进行解析。解析后的崩溃报告会包含崩溃发生的文件名、函数名和行号等详细信息,这些信息对于开发者来说是非常有价值的。
下方为博客找到的某个截图示例:
在这里插入图片描述

Crash类别以及解法分析

子线程访问UI而导致的崩溃

Objective-C是一种动态语言,它具有强大的运行时特性。我们可以利用这些特性,设计一套防护系统,以降低应用程序的崩溃率。具体来说,我们可以利用Method Swizzling等技术,对容易造成崩溃的系统方法进行拦截和修改,以达到避免和修复崩溃的目的。
例如,我们可以拦截UIView的setNeedsLayout和setNeedsDisplay方法,确保这些方法只在主线程中被调用。如果它们在子线程中被调用,程序将抛出异常或进行其他错误处理。这样就可以避免因子线程访问UI而导致的崩溃问题。不过我们尽量在做UI操作的时候,转到主线程去做处理。

unrecognized selector send to instance xxx

这样的错误,你可能并不陌生。
这种错误通常是因为调用了某个对象或者某个类里不存在的方法,从而触发了消息转发机制,最终把这个未识别的消息发送给了NSObject的默认实现。

例如调用以下一段代码就会产生crash

//test code
UIButton * testObj = [[UIButton alloc] init];
[testObj performSelector:@selector(someMethod:)];

报错如下:

在这里插入图片描述

runtime中具体的方法调用流程大致如下:

  1. 首先,在相应操作的对象中的缓存方法列表中找调用的方法,如果找到,转向相应实现并执行。
  2. 如果没找到,在相应操作的对象isa指针指向的类中的方法列表中找调用的方法,如果找到,转向相应实现执行。
  3. 如果没找到,去父类指针所指向的对象中执行1,2.
  4. 以此类推,如果一直到根类还没找到,转向拦截调用,走消息转发机制。

如果没有重写拦截调用的方法,程序报错。

所以,此类问题解决方案: 拦截调用
在方法调用中说到了,如果没有找到方法就会转向拦截调用。
那么什么是拦截调用呢?
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理:

+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

在这里插入图片描述

由上图可见,在一个函数找不到时,runtime提供了三种方式去补救:

  1. 调用resolveInstanceMethod给个机会让类添加这个实现这个函数
  2. 调用forwardingTargetForSelector让别的对象去执行这个函
  3. 调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。

如果都不中,调用doesNotRecognizeSelector抛出异常。

unrecognized selector crash 防护方案
既然可以补救,我们完全也可以利用消息转发机制来做文章。那么问题来了,在这三个步骤里面,选择哪一步去改造比较合适呢。

这里我们选择了第二步forwardingTargetForSelector来做文章。原因如下:
resolveInstanceMethod 需要在类的本身上动态添加它本身不存在的方法,这些方法对于该类本身来说冗余的
forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发选择机制,不适合多次重写
forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写
选择了forwardingTargetForSelector之后,可以将NSObject的该方法重写,做以下几步的处理:

  1. 动态创建一个桩类
  2. 动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
  3. 将消息直接转发到这个桩类对象上。

在这里插入图片描述

下方是一个动态创建类的代码示例:

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 动态创建一个类
        Class dynamicClass = objc_allocateClassPair([NSObject class], "DynamicClass", 0);
        
        if (!dynamicClass) {
            NSLog(@"Failed to allocate class pair");
            return -1;
        }
        
        // 注册这个类
        objc_registerClassPair(dynamicClass);
        
        // 动态创建一个实例
        id instance = [[dynamicClass alloc] init];
        NSLog(@"Instance of DynamicClass: %@", instance);
        
        // 动态添加方法
        class_addMethod(dynamicClass, @selector(sayHello), (IMP)sayHelloIMP, "v@:");
        
        // 调用动态添加的方法
        [instance sayHello];
    }
    return 0;
}

// 方法的实现
void sayHelloIMP(id self, SEL _cmd) {
    NSLog(@"Hello from DynamicClass!");
}

在这个示例中,我们首先使用objc_allocateClassPair创建一个新的类,然后使用objc_registerClassPair注册这个类。接着,我们动态添加了一个名为sayHello的方法,并调用它。
解释参数
objc_allocateClassPair:用于分配一个新的类对,第一个参数是父类,第二个参数是新类的名称,第三个参数是额外的内存大小(通常为0)。
objc_registerClassPair:用于注册这个类,使其可以被使用。
class_addMethod:用于向类添加一个新的方法。第一个参数是目标类,第二个参数是选择器(SEL),第三个参数是方法的实现(IMP),第四个参数是方法的签名(type encoding)。

KVO crash

KVO的addObserver和removeObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash。
苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,是一种比较理想的使用方式。

1、注册观察
在这里插入图片描述
2、实现回调方法
在这里插入图片描述

3、移除观察
在这里插入图片描述

KVO举例以及注意事项

//被观察者 StockData.m
#import "StockData.h"
@interface StockData()
@property(nonatomic, strong)NSString *stockName;
@property(nonatomic, strong)NSString *price;
@end

//观察者 SLVKVOController.m
#import "SLVKVOController.h"
#import "StockData.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.stockData setValue:@"searph" forKey:@"stockName"];
    [self.stockData setValue:@"10.0" forKey:@"price"];
    [self.stockData addObserver:self forKeyPath:@"price"  options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:SLVKVOContext];
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if(context == SLVKVOContext && object == self.stockData && [keyPath isEqualToString:@"price"]) {
        NSString * oldValue = [change objectForKey:NSKeyValueChangeOldKey];
        NSString * newValue = [change objectForKey:NSKeyValueChangeNewKey];
        self.myLabel.text = [NSString stringWithFormat:@"oldValue:%@ , newValue:%@",oldValue,newValue];
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

-(void)dealloc {
    [self.stockData removeObserver:self forKeyPath:@"price" context:SLVKVOContext];
}

KVO常见crash及防护方案

KVO常见crash类型:

1.不能对不存在的属性进行kvo观测,否则会报crash:uncaught exception 'NSUnknownKeyException', reason: '[<StockData 0x600000203d50> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key stockName.'
2. 订阅者必须写observeValueForKeyPath:ofObject:change:context:方法,否则crash。
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<SLVKVOController: 0x7f811372ff70>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
3.移除观察,超过addObserver的次数就会 crash:Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <SLVKVOController 0x7ff8e8703100> for the key path "price" from <StockData 0x60800003d000> because it is not registered as an observer.'

KVO crash解决方案:
首先为 NSObject 建立一个分类,利用 Method Swizzling,实现自定义的 BMP_addObserver:forKeyPath:options:context:BMP_removeObserver:forKeyPath:BMP_removeObserver:forKeyPath:context:BMPKVO_dealloc 方法,用来替换系统原生的添加移除观察者方法的实现。
然后在观察者和被观察者之间建立一个 KVODelegate 对象,两者之间通过 KVODelegate 对象 建立联系。然后在添加和移除操作时,在自定义的方法交换内部将 KVO 的相关信息例如 observer、keyPath、options、context 保存为 KVOInfo 对象,并添加到 KVODelegate 对象 中对应 的 关系哈希表 中,对应原有的添加观察者。

关系哈希表的数据结构:{keypath : [KVOInfo 对象1, KVOInfo 对象2, … ]}
在添加和移除操作的时候,利用 KVODelegate 对象 做转发,把真正的观察者变为 KVODelegate 对象,而当被观察者的特定属性发生了改变,再由 KVODelegate 对象 分发到原有的观察者上。
那么,BayMax 系统是如何避免 KVO 崩溃的呢?
添加观察者时:通过关系哈希表判断是否重复添加,只添加一次。
移除观察者时:通过关系哈希表是否已经进行过移除操作,避免多次移除。
观察键值改变时:同样通过关系哈希表判断,将改变操作分发到原有的观察者上。
另外,为了避免被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO 导致崩溃。BayMax 系统还利用 Method Swizzling 实现了自定义的 dealloc,在系统 dealloc 调用之前,将多余的观察者移除掉。

KVC造成的crash

场景1:key 不存在
在这里插入图片描述
在这里插入图片描述
防护方法:进行 KVC Crash 防护,我们就需要重写 setValue: forUndefinedKey: 方法和 valueForUndefinedKey: 方法。重写这两个方法之后,就可以防护key不存在的情况了。

场景2:key为nil
在这里插入图片描述
在这里插入图片描述

**防护方法:**可以利用 Method Swizzling 方法,在 NSObject 的分类中将setValue:forKey:和自定义的 ysc_setValue:forKey: 进行方法交换。然后在自定义的方法中,添加对 key 为 nil 这种类型的判断。

Person * person = [[Person alloc] init];
[person setValue:nil forKey:@“name”];
当value为nil的时候不会Crash.

NSTimer导致的Crash

NSTimer、CADisplayLink会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。

先来看看timer最常用的写法

@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)timerRun {
    NSLog(@"%s", __func__);
}
- (void)dealloc {
    [self.timer invalidate];
    NSLog(@"%s", __func__);
}
@end

循环引用了
在这里插入图片描述

解决方案1:使用weakSelf (这里使用的是timer的block方法)

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerRun];
    }];
}

在这里插入图片描述

解决方案2:加入了一个中间代理对象LJProxy,timer的target不直接是TimerViewController,而是持有LJProxy实例,让LJProxy实例来弱引用TimerViewController,timer强引用LJProxy实例. 而且代理类里面要重写消息转发方法去处理一下,要不然消息传递会找不到方法导致崩溃。

LJProxy可以继承自NSObject,也可以继承自NSProxy,但是内部代码处理会有所不同。
如果继承自NSProxy,会实现下面的方法,消息转发方法需要实现这两个。

在这里插入图片描述
在这里插入图片描述

如果继承自NSObject,会实现下面的方法,消息转发方法需要实现这一个即可。

@interface LJProxy : NSObject
+ (instancetype) proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation LJProxy
+ (instancetype) proxyWithTarget:(id)target
{
    LJProxy *proxy = [[LJProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
    //如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
}
@end
- (void)viewDidLoad {
    [super viewDidLoad];
    // 这里的target发生了变化
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

由于NSProxy专门用来做消息转发的,效率高,因为这个内部直接去消息转发,调用methodSignature…,如果继承自NSObject,里面调用会先去父类里面搜索,没有的话才会去做消息转发。
所以这里建议做代理类的时候直接继承自NSProxy的就可以,这样子是最好的。

解决方案3: 及时的把timer销毁,即:调用 [self.timer inbalidate]; 例如 识别到当前控制器返回按钮点击的时候 等等。 (该方法不推荐,代码混乱,不具有统一性。)

解决方案4:「出自 高性能iOS开发 一书」。
在这里插入图片描述

这里的间接层和 方案2类似,但是 只不过是在间接层里边进行 timer的创建以及timer的销毁。

控制器直接调用间接层并传入self「后边会赋给delegate<weak引用>」和selector。 间接层会创建timer并倒计时处理事件。 处理事件之后会 通过delegate 调用selector. [self.delegate performSelector:@selector(self.selector) withObject:想要传的值];

这里我觉得方案4是最容易理解的。也是最容易实施的。

方案4代码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

野指针

  • 野指针就是指向一个被释放或者被回收的对象,但是指向该对象的指针没有任何修改,以致于该指针让指向已经回收后的内存地址。
  • 其中访问野指针是没有问题的,使用野指针的时候会出现Crash,样例如下:

在这里插入图片描述

这是网友总结的,有兴趣的可以看下:www.jianshu.com/p/9fd4dc046… 本人,也就是看看乐呵,其原理啥的,见仁见智吧。开发行业太j8难了!

Watch Dog超时造成的crash

这种崩溃通常比较容易分辨,因为错误码是固定的0x8badf00d。(程序员也有幽默的一面,他们把它读作Ate Bad Food。)在iOS上,它经常出现在执行一个同步网络调用而阻塞主线程的情况。因此,永远不要进行同步网络调用。

其他crash待补充



参考文章:

crash收集:https://sentry.io/for/ios/
crash日志分析:https://developer.volcengine.com/articles/7062608853434630152
方法找不到解决方案:https://neyoufan.github.io/2017/01/13/ios/BayMax_HTSafetyGuard/
消息转发机制以及避免崩溃方案:https://blog.csdn.net/mumubumaopao/article/details/108113405
kvo crash解决方案:https://juejin.cn/post/6844903927469588488
https://github.com/itcharge/YSC-Avoid-Crash
kvc 防护:https://blog.csdn.net/lianai911/article/details/103400862
NSTimer循环引用问题处理:https://www.jianshu.com/p/d4589134358a
野指针定位:https://www.jianshu.com/p/9fd4dc046046?utm_source=oschina-app

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

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

相关文章

RK3576 Android14 状态栏和导航栏增加显示控制功能

问题背景&#xff1a; 因为RK3576 Android14用户需要手动控制状态栏和导航栏显示隐藏控制&#xff0c;包括对锁屏后下拉状态栏的屏蔽&#xff0c;在设置功能里增加此功能的控制&#xff0c;故参考一些博客完成此功能&#xff0c;以下是具体代码路径的修改内容。 解决方案&…

【Rust自学】13.5. 迭代器 Pt.1:迭代器的定义、iterator trait和next方法

13.5.0. 写在正文之前 Rust语言在设计过程中收到了很多语言的启发&#xff0c;而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。 在本章中&#xff0c;我们会讨论 Rust 的一…

LabVIEW 蔬菜精密播种监测系统

在当前蔬菜播种工作中&#xff0c;存在着诸多问题。一方面&#xff0c;播种精度难以达到现代农业的高标准要求&#xff0c;导致种子分布不均&#xff0c;影响作物的生长发育和最终产量&#xff1b;另一方面&#xff0c;对于小粒径种子&#xff0c;传统的监测手段难以实现有效监…

2024年年终总结——坎坷与坚持,焦虑与收获

不知不觉间&#xff0c;2024年已经悄然过去&#xff0c;回望这一年的时间&#xff0c;一时间竟感觉混混沌沌无法形容&#xff0c;选择一些时间坐下来让自己简单回忆一下自己的2024。 先简单回望一下24年一整年的工作情况&#xff1a; 24年一开始&#xff0c;工作最期待的的节点…

无人机技术架构剖析!

一、飞机平台系统 飞机平台系统是无人机飞行的主体平台&#xff0c;主要提供飞行能力和装载功能。它由机体结构、动力装置、电气设备等组成。 机体结构&#xff1a;无人机的机身是其核心结构&#xff0c;承载着其他各个组件并提供稳定性。常见的机身材料包括碳纤维、铝合金、…

springboot基于微信小程序的传统美食文化宣传平台小程序

Spring Boot 基于微信小程序的传统美食文化宣传平台 一、平台概述 Spring Boot 基于微信小程序的传统美食文化宣传平台是一个集传统美食展示、文化传承、美食制作教程分享、用户互动交流以及美食相关活动推广为一体的综合性线上平台。它借助 Spring Boot 强大的后端开发框架构…

Android系统开发(八):从麦克风到扬声器,音频HAL框架的奇妙之旅

引言&#xff1a;音浪太强&#xff0c;我稳如老 HAL&#xff01; 如果有一天你的耳机里传来的不是《咱们屯里人》&#xff0c;而是金属碰撞般的杂音&#xff0c;那你可能已经感受到了 Android 音频硬件抽象层 (HAL) 出问题的后果&#xff01;在 Android 音频架构中&#xff0c…

51.WPF应用加图标指南 C#例子 WPF例子

完整步骤&#xff1a; 先使用文心一言生成一个图标如左边使用Windows图片编辑器编辑&#xff0c;去除背景使用正方形&#xff0c;放大图片使图标铺满图片使用格式工程转换为ico格式&#xff0c;分辨率为最大 在资源管理器中右键项目添加ico类型图片到项目里图片属性设置为始终…

运行fastGPT 第四步 配置ONE API 添加模型

上次已经装好了所有的依赖和程序。 下面在网页中配置One API &#xff0c;这个是大模型的接口。配置好了之后&#xff0c;就可以配置fastGPT了。 打开 OneAPI 页面 添加模型 这里要添加具体的付费模型的API接口填进来。 可以通过ip:3001访问OneAPI后台&#xff0c;**默认账号…

道旅科技借助云消息队列 Kafka 版加速旅游大数据创新发展

作者&#xff1a;寒空、横槊、娜米、公仪 道旅科技&#xff1a;科技驱动&#xff0c;引领全球旅游分销服务 道旅科技 &#xff08;https://www.didatravel.com/home&#xff09; 成立于 2012 年&#xff0c;总部位于中国深圳&#xff0c;是一家以科技驱动的全球酒店资源批发商…

51单片机——DS18B20温度传感器

由于DS18B20数字温度传感器是单总线接口&#xff0c;所以需要使用51单片机的一个IO口模拟单总线时序与DS18B20通信&#xff0c;将检测的环境温度读取出来 1、DS18B20模块电路 传感器接口的单总线管脚接至单片机P3.7IO口上 2、DS18B20介绍 2.1 DS18B20外观实物图 管脚1为GN…

Redis的安装和配置、基本命令

一、实验目的 本实验旨在帮助学生熟悉Redis的安装、配置和基本使用&#xff0c;包括启动Redis服务、使用命令行客户端进行操作、配置Redis、进行多数据库操作以及掌握键值相关和服务器相关的命令。 二、实验环境准备 1. JAVA环境准备&#xff1a;确保Java Development Kit …

2、ansible的playbook

ansible的脚本&#xff1a;playbook剧本 脚本的作用&#xff1a;复用 playbook的组成部分 1、开头 ---&#xff1a;表示是一个yaml文件&#xff0c;但是可以忽略。 2、Tasks&#xff08;任务&#xff09;&#xff1a;包含了目标主机上执行的操作&#xff0c;操作还是由模板来…

vscode的安装与使用

下载 地址&#xff1a;https://code.visualstudio.com/ 安装 修改安装路径&#xff08;不要有中文&#xff09; 点击下一步&#xff0c;创建桌面快捷方式&#xff0c;等待安装 安装中文插件 可以根据自己的需要安装python和Jupyter插件

Java : 各版本 jdk 下载及环境变量配置

--------------------------一、 JDK下载 ---------------------------- JDK下载地址&#xff1a;&#xff08;没有账号提示注册&#xff0c;最好用Chrome 浏览器&#xff09; Java Archive | Oracle 选择版本安装&#xff1a;&#xff08;注意不同系统&#xff09; 下载后按照…

IoTDB 查询时报可用内存不足

现象 IoTDB 3C3D 集群中&#xff0c;进行查询时报可用内存不足&#xff0c;即使是 show devices 这样简单的查询也会报内存不足。 原因 客户目前使用的 JDK 版本是 1.8, 该版本 JDK 对 GC 控制效果不佳&#xff0c;有可能出现可用内存不足的情况&#xff0c;同时 GC 耗时较长…

Jmeter 简单使用、生成测试报告(一)

一、下载Jmter 去官网下载&#xff0c;我下载的是apache-jmeter-5.6.3.zip&#xff0c;解压后就能用。 二、安装java环境 JMeter是基于Java开发的&#xff0c;运行JMeter需要Java环境。 1.下载JDK、安装Jdk 2.配置java环境变量 3.验证安装是否成功&#xff08;java -versio…

LabVIEW时域近场天线测试

随着通信技术的飞速发展&#xff0c;特别是在5G及未来通信技术中&#xff0c;天线性能的测试需求日益增加。对于短脉冲天线和宽带天线的时域特性测试&#xff0c;传统的频域测试方法已无法满足其需求。时域测试方法在这些应用中具有明显优势&#xff0c;可以提供更快速和精准的…

SSE 实践:用 Vue 和 Spring Boot 实现实时数据传输

前言 大家好&#xff0c;我是雪荷。最近我在灵犀 BI 项目中引入了 SSE 技术&#xff0c;以保证图表的实时渲染&#xff0c;当图表渲染完毕服务端推送消息至浏览器端触发重新渲染。 什么是 SSE&#xff1f; SSE 全称为 Server-Send Events 意思是服务端推送事件。 SSE 相比于 …

hive连接mysql报错:Unknown version specified for initialization: 3.1.0

分享下一些报错的可能原因吧 1.要开启hadoop 命令&#xff1a;start-all.sh 2.检查 hive-site.xml 和 hive-env.sh。 hive-site.xml中应设置自己mysql的用户名和密码 我的hive-site.xml如下&#xff1a; <configuration><property><name>javax.jdo.opt…