当前位置: 首页 > article >正文

kotlin 01flow-StateFlow 完整教程

一 Android StateFlow 完整教程:从入门到实战

StateFlow 是 Kotlin 协程库中用于状态管理的响应式流,特别适合在 Android 应用开发中管理 UI 状态。本教程将带全面了解 StateFlow 的使用方法。

1. StateFlow 基础概念

1.1 什么是 StateFlow?

StateFlow 是 Kotlin 协程提供的一种热流(Hot Flow),它具有以下特点:

  • 总是有当前值(初始值必须提供)
  • 只保留最新值
  • 支持多个观察者
  • 与 LiveData 类似但基于协程

1.2 StateFlow vs LiveData

特性StateFlowLiveData
生命周期感知否(需配合 lifecycleScope)
需要初始值
基于协程观察者模式
线程控制通过 Dispatcher主线程
背压处理自动处理自动处理

2. 基本使用

2.1 添加依赖

在 build.gradle 中添加:

dependencies {implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
}

2.2 创建 StateFlow

class MyViewModel : ViewModel() {// 私有可变的StateFlowprivate val _uiState = MutableStateFlow<UiState>(UiState.Loading)// 公开不可变的StateFlowval uiState: StateFlow<UiState> = _uiState.asStateFlow()sealed class UiState {object Loading : UiState()data class Success(val data: String) : UiState()data class Error(val message: String) : UiState()}fun loadData() {viewModelScope.launch {_uiState.value = UiState.Loadingtry {val result = repository.fetchData()_uiState.value = UiState.Success(result)} catch (e: Exception) {_uiState.value = UiState.Error(e.message ?: "Unknown error")}}}
}

2.3 在 Activity/Fragment 中收集 StateFlow

class MyActivity : AppCompatActivity() {private val viewModel: MyViewModel by viewModels()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.uiState.collect { state ->when (state) {is MyViewModel.UiState.Loading -> showLoading()is MyViewModel.UiState.Success -> showData(state.data)is MyViewModel.UiState.Error -> showError(state.message)}}}}}private fun showLoading() { /*...*/ }private fun showData(data: String) { /*...*/ }private fun showError(message: String) { /*...*/ }
}

3. 高级用法

3.1 结合 SharedFlow 处理一次性事件

class EventViewModel : ViewModel() {private val _events = MutableSharedFlow<Event>()val events = _events.asSharedFlow()sealed class Event {data class ShowToast(val message: String) : Event()object NavigateToNextScreen : Event()}fun triggerEvent() {viewModelScope.launch {_events.emit(Event.ShowToast("Hello World!"))}}
}// 在Activity中收集
lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.events.collect { event ->when (event) {is EventViewModel.Event.ShowToast -> showToast(event.message)EventViewModel.Event.NavigateToNextScreen -> navigateToNext()}}}
}

3.2 状态合并 (combine)

val userName = MutableStateFlow("")
val userAge = MutableStateFlow(0)val userInfo = combine(userName, userAge) { name, age ->"Name: $name, Age: $age"
}// 收集合并后的流
userInfo.collect { info ->println(info)
}

3.3 状态转换 (map, filter, etc.)

val numbers = MutableStateFlow(0)val evenNumbers = numbers.filter { it % 2 == 0 }.map { "Even: $it" }evenNumbers.collect { println(it) }

4. 性能优化

4.1 使用 stateIn 缓存 StateFlow

val networkFlow = flow {// 模拟网络请求emit(repository.fetchData())
}val cachedState = networkFlow.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000), // 5秒无订阅者停止initialValue = "Loading..."
)

4.2 避免重复收集

// 错误方式 - 每次重组都会创建新的收集器
@Composable
fun MyComposable(viewModel: MyViewModel) {val state by viewModel.state.collectAsState()// ...
}// 正确方式 - 使用 derivedStateOf 或 remember
@Composable
fun MyComposable(viewModel: MyViewModel) {val state by remember { viewModel.state }.collectAsState()// ...
}

5. 测试 StateFlow

5.1 单元测试

@Test
fun `test state flow`() = runTest {val viewModel = MyViewModel()val results = mutableListOf<MyViewModel.UiState>()val job = launch {viewModel.uiState.collect { results.add(it) }}viewModel.loadData()advanceUntilIdle()assertEquals(3, results.size) // Loading, Success/ErrorassertTrue(results[0] is MyViewModel.UiState.Loading)job.cancel()
}

5.2 使用 Turbine 测试库

dependencies {testImplementation "app.cash.turbine:turbine:0.12.1"
}@Test
fun `test with turbine`() = runTest {val viewModel = MyViewModel()viewModel.uiState.test {viewModel.loadData()assertEquals(MyViewModel.UiState.Loading, awaitItem())val success = awaitItem()assertTrue(success is MyViewModel.UiState.Success)cancelAndIgnoreRemainingEvents()}
}

6. 常见问题解答

Q1: StateFlow 和 LiveData 哪个更好?

StateFlow 更适合协程环境,LiveData 更简单但功能较少。新项目推荐 StateFlow。

Q2: 如何处理背压(Backpressure)?

StateFlow 自动处理背压,只保留最新值。

Q3: 为什么我的收集器没有收到更新?

检查:

  1. 是否在正确的生命周期范围内收集
  2. Flow 是否有发射新值
  3. 是否在正确的协程上下文中

Q4: 如何避免内存泄漏?

使用 repeatOnLifecycleflowWithLifecycle 确保只在活跃生命周期收集。

7. 完整示例项目

以下是一个完整的 ViewModel 示例:

class UserViewModel(private val userRepository: UserRepository) : ViewModel() {private val _userState = MutableStateFlow<UserState>(UserState.Loading)val userState: StateFlow<UserState> = _userState.asStateFlow()private val _events = MutableSharedFlow<UserEvent>()val events: SharedFlow<UserEvent> = _events.asSharedFlow()init {loadUser()}fun loadUser() {viewModelScope.launch {_userState.value = UserState.Loadingtry {val user = userRepository.getUser()_userState.value = UserState.Success(user)} catch (e: Exception) {_userState.value = UserState.Error(e.message ?: "Unknown error")_events.emit(UserEvent.ShowErrorToast("Failed to load user"))}}}fun updateUserName(name: String) {viewModelScope.launch {val currentUser = (_userState.value as? UserState.Success)?.user ?: return@launchval updatedUser = currentUser.copy(name = name)_userState.value = UserState.Success(updatedUser)userRepository.updateUser(updatedUser)}}sealed class UserState {object Loading : UserState()data class Success(val user: User) : UserState()data class Error(val message: String) : UserState()}sealed class UserEvent {data class ShowErrorToast(val message: String) : UserEvent()}
}

通过本教程,应该已经掌握了 StateFlow 的核心用法。StateFlow 是构建响应式 Android 应用的强大工具,结合协程可以提供更简洁、更安全的状态管理方案。

二 stateFlow和sharedFlow的区别

关于 StateFlowSharedFlow 的问题非常关键。它们都是 Kotlin Flow 的“热流”(hot stream)变种,广泛用于 事件驱动编程状态管理。下面逐条详细解答问题:


✅ 1. StateFlow 和 SharedFlow 的区别?能否互相转换?

特性StateFlowSharedFlow
是否需要初始值✅ 是(必须有)❌ 否(可选)
是否有当前值 .value✅ 是❌ 没有
是否支持粘性订阅(可立即获取最新值)✅ 是(始终有当前值)❌ 否(默认非粘性,取决于 replay)
用于什么状态管理(UI 状态)一次性事件(导航、吐司等)
是否自动重发✅ 是:新订阅者立即拿到当前值❌ 否(需设置 replay > 0 才能重发)
是否支持多次发送相同值❌ 否(相同值不会触发 collect)✅ 是
是否丢失事件❌ 不会丢(始终保留一个最新值)✅ 可能丢(如果没有设置 replay 或 buffer)

🔁 能否互相转换?

不能直接转换,因为它们底层设计不同。但可以人为地模拟:

val sharedFlow = MutableSharedFlow<Int>(replay = 1)
val stateFlow = sharedFlow.stateIn(scope, SharingStarted.Eagerly, initialValue)

或者从 StateFlow 获取 SharedFlow 的行为:

val stateFlow: StateFlow<Int> = MutableStateFlow(0)
val sharedFlow = stateFlow.shareIn(scope, SharingStarted.Lazily)

✅ 2. 是不是 StateFlow 就是 SharedFlow 多了 .value 和初始值?

可以这么说,但这只是表面。它们的语义不同:

  • StateFlow 表示的是状态的容器(如 ViewModel 中的 UI 状态)。
  • SharedFlow 更像是事件的通道,没有“当前状态”的概念。
// StateFlow 的特点
println(stateFlow.value) // 始终可以拿到当前值// SharedFlow 没有 value,只能 collect
sharedFlow.collect { event -> ... }

✅ 3. 如何处理“被压的”问题(Backpressure、丢失事件)?

📌 StateFlow

  • 没有丢失问题:永远保留最后一个值。
  • 新订阅者会立刻收到当前值。
  • 不支持 backpressure,因为没有队列:只有一个值。
val sf = MutableStateFlow(0)
sf.value = 1
sf.value = 2
// collect 会立即收到 2(中间的1不会被保留)

📌 SharedFlow

  • 默认不保留任何事件replay = 0,无 buffer)。
  • 如果事件很快发送但没人 collect,就直接丢失
  • 可以设置 replayextraBufferCapacity 来控制:
val shared = MutableSharedFlow<Int>(replay = 2,extraBufferCapacity = 5
)
  • 设置 onBufferOverflow = BufferOverflow.DROP_OLDEST/ DROP_LATEST / SUSPEND 控制丢弃策略。

✅ 使用建议总结

需求推荐使用
需要保存并暴露当前状态(如 UI 状态)StateFlow
需要发送一次性事件,如 Toast、导航SharedFlow
想在订阅时立即拿到最后值StateFlow
想发送多个相同值SharedFlow(StateFlow 会去重)

三 MutableStateFlow 是如何更新值的?

_seatLevel.value = newValue_seatLevel.update { ... } 都可以用来更新 MutableStateFlow 的值,但它们在 并发安全性使用场景 上有区别:


1. _seatLevel.value = newValue(直接赋值)

  • 作用:直接设置 MutableStateFlow 的新值。
  • 特点
    • 非原子操作:如果在多线程环境下,直接赋值可能会导致竞态条件(Race Condition),因为 value 的读写不是线程安全的。
    • 简单直接:适用于单线程或确定不会并发更新的场景。
  • 示例
    _seatLevel.value = 5 // 直接设置新值
    

2. _seatLevel.update { ... }(原子更新)

  • 作用:以 原子方式 计算并更新 MutableStateFlow 的值。
  • 特点
    • 原子操作update 是线程安全的,内部使用 CAS (Compare-And-Swap) 机制,确保在并发环境下不会出现数据竞争。
    • 基于当前值计算:可以访问当前值(current),并返回新值。
    • 适合条件更新:适用于需要依赖当前值进行计算的场景(如递增、条件过滤等)。
  • 示例
    _seatLevel.update { current ->if (newLevel in 0..100) newLevel else current
    }
    
    这里:
    • current 是当前 _seatLevel 的值。
    • 返回的值会作为新值(如果 newLevel 不在 0..100 范围内,则保持原值)。

关键区别

特性_seatLevel.value = newValue_seatLevel.update { ... }
线程安全❌ 非原子操作,可能竞态条件✅ 原子操作,线程安全
是否依赖当前值❌ 直接赋值,不关心当前值✅ 可以访问当前值并计算新值
适用场景单线程或确定无并发更新多线程环境或需要条件更新

如何选择?

  1. 如果只是简单赋值,且确定不会并发更新(例如 UI 线程中),可以直接用 value = newValue
  2. 如果涉及多线程,或需要基于当前值计算新值(如递增、条件过滤等),应该用 update { ... }

底层实现

  • update 内部使用了 while (true) + compareAndSet 机制,确保原子性:
    public inline fun update(function: (T) -> T) {while (true) {val prevValue = valueval nextValue = function(prevValue)if (compareAndSet(prevValue, nextValue)) {return}}
    }
    
    它会不断尝试更新,直到成功(避免并发冲突)。

总结

  • value = newValue:简单直接,但不适合并发场景。
  • update { ... }:线程安全,适合多线程或需要依赖当前值的更新。

在你的 updateLevel 函数中,由于涉及条件判断(newLevel in 0..100),使用 update 是更安全、更合理的选择。

四 为什么update 更新是原子性?

相关代码

public inline fun update(function: (T) -> T) {while (true) {val prevValue = valueval nextValue = function(prevValue)if (compareAndSet(prevValue, nextValue)) {return}}
} 

这段代码之所以能保证 MutableStateFlow 的更新是 原子性(Atomic) 的,关键在于它使用了 CAS(Compare-And-Swap) 机制,并通过 while (true) 循环确保最终成功更新。我们逐行分析其原子性保证:


1. compareAndSet(prevValue, nextValue) 是关键

compareAndSetAtomic 操作(底层基于 CPU 的 CAS 指令),它的行为是:

fun compareAndSet(expect: T, newValue: T): Boolean
  • 如果当前 value == expect(说明其他线程尚未修改),则更新为 newValue 并返回 true
  • 如果当前 value != expect(说明其他线程已经修改),则放弃更新并返回 false

由于 compareAndSet硬件级原子操作,它保证了:

  • 读取 value比较 expect写入 newValue 这三个步骤是 不可分割的(不会被其他线程打断)。

2. while (true) 循环的作用

如果 compareAndSet 失败(返回 false),说明其他线程已经修改了 value,此时:

  1. 重新读取最新的 valueval prevValue = value)。
  2. 重新计算 nextValuefunction(prevValue))。
  3. 再次尝试 compareAndSet,直到成功为止。

这种 乐观锁(Optimistic Locking) 策略确保了:

  • 即使多线程竞争,最终所有更新都会 按顺序应用(不会丢失任何修改)。
  • 不会发生 死锁(因为没有阻塞,只是重试)。

3. 为什么能保证原子性?

  • compareAndSet 是原子的:单次 compareAndSet 调用是线程安全的。
  • 循环直到成功:即使其他线程并发修改,当前线程最终会基于最新的值成功更新。
  • 函数式更新function(prevValue) 的计算是基于最新的 prevValue,不会出现脏数据。

4. 对比非原子更新的问题

如果直接用 value = newValue

// 非原子操作,可能导致竞态条件
_seatLevel.value = _seatLevel.value + 1

在多线程环境下:

  1. 线程 A 读取 value = 0
  2. 线程 B 读取 value = 0
  3. 线程 A 写入 value = 1
  4. 线程 B 写入 value = 1(本应是 2,但结果错误!)。

update 能避免这个问题:

_seatLevel.update { current -> current + 1 } // 线程安全

5. 原子性的直观体现

从代码中可以看出原子性的关键点:

  1. val prevValue = value:读取当前值(可能被其他线程修改)。
  2. val nextValue = function(prevValue):基于当前值计算新值。
  3. if (compareAndSet(prevValue, nextValue))
    • 只有 value 仍等于 prevValue 时才会更新。
    • 如果失败(说明其他线程已修改),则 重试,确保最终更新是基于最新值。

6. 类似机制的对比

  • Java 的 AtomicInteger
    atomicInt.updateAndGet(x -> x + 1); // 同样基于 CAS
    
  • 数据库的乐观锁
    UPDATE table SET value = newValue WHERE value = oldValue; -- 类似 CAS
    

总结

这段代码的原子性由以下两点保证:

  1. compareAndSet 的原子性:确保比较和更新的操作不可分割。
  2. 循环重试机制:确保并发冲突时最终能成功更新。

因此,update { ... } 是线程安全的,适合在多线程环境下使用,而直接 value = newValue 不是。

相关文章:

kotlin 01flow-StateFlow 完整教程

一 Android StateFlow 完整教程&#xff1a;从入门到实战 StateFlow 是 Kotlin 协程库中用于状态管理的响应式流&#xff0c;特别适合在 Android 应用开发中管理 UI 状态。本教程将带全面了解 StateFlow 的使用方法。 1. StateFlow 基础概念 1.1 什么是 StateFlow? StateF…...

1.2.1 Linux音频系统发展历程简介

Linux音频系统的发展经历了从最初的简单驱动到今天多层次、模块化音频架构。简要梳理其主要历程&#xff1a; 早期的OSS&#xff08;Open Sound System&#xff09; 在90年代及2000年代初&#xff0c;Linux主要使用OSS来支持音频。OSS直接为硬件设备&#xff08;如声卡&#…...

浏览器刷新结束页面事件,调结束事件的接口(vue)

浏览器刷新的时候&#xff0c;正在进行中的事件结束掉&#xff0c;在刷新浏览器的时候做一些操作。 如果是调接口&#xff0c;就不能使用axios封装的接口&#xff0c;需要使用原生的fetch。 找到公共的文件App.vue 使用window.addEventListener(‘beforeunload’, function (e…...

聊聊Spring AI Alibaba的SentenceSplitter

序 本文主要研究一下Spring AI Alibaba的SentenceSplitter SentenceSplitter spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/transformer/splitter/SentenceSplitter.java public class SentenceSplitter extends TextSplitter {private final EncodingRegis…...

前端-什么是结构语言、样式语言、脚本语言?

目录 1. 结构语言&#xff08;HTML / WXML&#xff09;——房子的骨架 2. 样式语言&#xff08;CSS / WXSS&#xff09;——房子的装修 3. 脚本语言&#xff08;JavaScript&#xff09;——房子的智能控制系统 总结对比表&#xff1a; 1. 结构语言&#xff08;HTML / WXML&a…...

LLM论文笔记 28: Universal length generalization with Turing Programs

Arxiv日期&#xff1a;2024.10.4机构&#xff1a;Harvard University 关键词 图灵机 CoT 长度泛化 核心结论 Turing Programs 的提出 提出 Turing Programs&#xff0c;一种基于图灵机计算步骤的通用 CoT 策略。通过将算法任务分解为逐步的“磁带更新”&#xff08;类似图灵…...

AI日报 · 2025年5月07日|谷歌发布 Gemini 2.5 Pro 预览版 (I/O 版本),大幅提升编码与视频理解能力

1、谷歌发布 Gemini 2.5 Pro 预览版 (I/O 版本)&#xff0c;大幅提升编码与视频理解能力 谷歌于5月6日提前发布 Gemini 2.5 Pro 预览版 (I/O 版本)&#xff0c;为开发者带来更强编码能力&#xff0c;尤其优化了前端与UI开发、代码转换及智能体工作流构建&#xff0c;并在WebDe…...

指定Docker镜像源,使用阿里云加速异常解决

yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo异常贴图 yum-config-manager&#xff1a;找不到命令 因为系统默认没有安装这个命令&#xff0c;这个命令在yum-utils 包里&#xff0c;可以通过命令yum -y install yum-util…...

VITA STANDARDS LIST,VITA 标准清单下载

VITA STANDARDS LIST&#xff0c;VITA 标准清单下载 DesignationTitleAbstractStatusVMEbus Handbook, 4th EditionA users guide to the VME, VME64 and VME64x bus specifications - features over 70 product photos and over 160 circuit diagrams, tables and graphs. The…...

Python从入门到高手8.3节-元组的常用操作方法

目录 11.3.1 元组的常用操作方法 11.3.2 元组的查找 11.3.3 祈祷明天不再打雷下雨 11.3.1 元组的常用操作方法 元组类型是一种抽象数据类型&#xff0c;抽象数据类型定义了数据类型的操作方法&#xff0c;在本节的内容中&#xff0c;着重介绍元组类型的操作方法。 ​ 元组是…...

Linux系统安装PaddleDetection

一、安装cuda 1. 查看设备 先输入nvidia-smi&#xff0c;查看设备支持的最大cuda版本&#xff0c;选择官网中支持的cuda版本 https://www.paddlepaddle.org.cn/install/quick?docurl/documentation/docs/zh/install/conda/linux-conda.html 2. 下载CUDA并安装 使用快捷键…...

【漫话机器学习系列】239.训练错误率(Training Error Rate)

机器学习基础概念 | 训练错误率&#xff08;Training Error Rate&#xff09;详解 在机器学习模型训练过程中&#xff0c;评估模型性能是至关重要的一个环节。其中&#xff0c;训练错误率&#xff08;Training Error Rate&#xff09; 是最基础也最重要的性能指标之一。 本文将…...

Vue3路由模式为history,使用nginx部署上线是空白的问题

一、问题 将vue使用打包后 npm run build将dist文件的内容&#xff0c;放入nginx的html中&#xff0c;并在nginx.conf中&#xff0c;设置端口 启动nginx&#xff0c;打开发现网页内容为空白 二、解决问题 1.配置vue-route const router createRouter({history: createWe…...

Python 数据智能实战 (13):AI的安全可靠 - 电商数据智能的红线与指南

写在前面 —— 技术向善,行稳致远:在智能时代,坚守数据伦理,构建可信赖的 AI 应用 通过前面的篇章,我们已经深入探索了如何利用 Python 和大语言模型 (LLM) 挖掘电商数据的巨大潜力,从智能用户分群到语义推荐,再到个性化内容生成和模型效果评估。我们手中的工具越来越…...

OpenCV 图形API(80)图像与通道拼接函数-----仿射变换函数warpAffine()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 对图像应用仿射变换。 函数 warpAffine 使用指定的矩阵对源图像进行变换&#xff1a; dst ( x , y ) src ( M 11 x M 12 y M 13 , M 21 x M…...

数据结构与算法:图论——最短路径

最短路径 先给出一些leetcode算法题&#xff0c;以后遇见了相关题目再往上增加 最短路径的4个常用算法是Floyd、Bellman-Ford、SPFA、Dijkstra。不同应用场景下&#xff0c;应有选择地使用它们&#xff1a; 图的规模小&#xff0c;用Floyd。若边的权值有负数&#xff0c;需要…...

提示词工程:通向AGI时代的人机交互艺术

‌引言&#xff1a;从基础到精通的提示词学习之旅‌ 欢迎来到 ‌"AGI时代核心技能"‌ 系列课程的第二模块——‌提示词工程‌。在这个模块中&#xff0c;我们将系统性地探索如何通过精心设计的提示词&#xff0c;释放大型语言模型的全部潜力&#xff0c;实现高效、精…...

FreeRTOS系统CPU使用率统计

操作系统中CPU使用率是在软件架构设计中必须要考虑的一个重要性能指标。它直接影响到程序的执行时间以及优先级更高的任务能否实时响应的问题。而CPU使用率也不能过低&#xff0c;避免资源浪费。 基本原理 操作系统会统计系统总共运行了多少时间&#xff0c;以及在此期间每个任…...

是更换Window资源管理器的时候了-> Files-community/Files

Files • 主页https://files.community/ 它已经做到了 云盘文件集成、标签页和多种布局、丰富的文件预览…… 您想要的一切现代文件管理器的强大功能&#xff0c; Files 都能做到。 概述 Files 是一个现代文件管理器&#xff0c;可帮助用户组织他们的文件和文件夹。Files 的…...

基于windows安装MySQL8.0.40

基于windows安装MySQL8.0.40 基于windows 安装 MySQL8.0.40&#xff0c;解压文件到D:\mysql-8.0.40-winx64 在D:\mysql-8.0.40-winx64目录下创建my.ini文件&#xff0c;并更新一下内容 [client] #客户端设置&#xff0c;即客户端默认的连接参数 # 设置mysql客户端连接服务…...

【Vue】组件自定义事件 TodoList 自定义事件数据传输

目录 一、绑定 二、解绑 组件自定义事件总结 TodoList案例对数据传输事件的修改 总结不易~ 本章节对我有很大收获&#xff0c; 希望对你也是&#xff01;&#xff01;&#xff01; 本章节素材已上传Gitee&#xff1a;yihaohhh/我爱Vue - Gitee.com 前面我们学习的clikc、…...

基于Centos7的DHCP服务器搭建

一、准备实验环境&#xff1a; 克隆两台虚拟机 一台作服务器&#xff1a;DHCP Server 一台作客户端&#xff1a;DHCP Clinet 二、部署服务器 在网络模式为NAT下使用yum下载DHCP 需要管理员用户权限才能下载&#xff0c;下载好后关闭客户端&#xff0c;改NAT模式为仅主机模式…...

LabVIEW超声波液位计检定

在工业生产、运输和存储等环节&#xff0c;液位计的应用十分广泛&#xff0c;其中超声波液位计作为非接触式液位测量设备备受青睐。然而&#xff0c;传统立式水槽式液位计检定装置存在受建筑高度影响、量程范围受限、流程耗时长等问题&#xff0c;无法满足大量程超声波液位计的…...

Ubuntu 24.04 完整Docker安装指南:从零配置到实战命令大全

文章目录 1. 安装 Docker2. 配置 Docker 镜像加速器2.1 配置 Docker 镜像源2.2 重启 Docker 服务 3. Docker 常用命令3.1 Docker 常用命令速查表3.1.1 容器管理3.1.2 镜像管理3.1.3 网络管理3.1.4 数据卷管理3.1.5 容器资源管理3.1.6 Docker Compose&#xff08;容器编排&#…...

[STM32] 4-2 USART与串口通信(2)

文章目录 前言4-2 USART与串口通信(2)数据发送过程双缓冲与连续发送数据发送过程中的问题 数据接收过程TXE标志位&#xff08;发送数据寄存器空&#xff09;TC标志位&#xff08;发送完成标志位&#xff09;单个数据的发送数据的连续发送 接收过程中遇到的问题问题描述&#xf…...

基于Python+MongoDB猫眼电影 Top100 数据爬取与存储

前言&#xff1a;从猫眼电影排行榜页面&#xff08;TOP100榜 - 猫眼电影 - 一网打尽好电影 &#xff09;爬取 Top100 电影的电影名称、图片地址、主演、上映时间和评分等关键信息&#xff0c;并将这些信息存储到本地 MongoDB 数据库中&#xff0c;&#x1f517; 相关链接Xpath&…...

前端缓存踩坑指南:如何优雅地解决浏览器缓存问题?

浏览器缓存&#xff0c;配置得当&#xff0c;它能让页面飞起来&#xff1b;配置错了&#xff0c;一次小小的上线&#xff0c;就能把你扔进线上 bug 的坑里。你可能遇到过这些情况&#xff1a; 部署上线了&#xff0c;结果用户还在加载旧的 JS&#xff1b;接口数据改了&#xf…...

Ubuntu 单机多卡部署脚本: vLLM + DeepSeek 70B

# 部署脚本&#xff1a;Ubuntu vLLM DeepSeek 70B # 执行前请确保&#xff1a;1. 系统为 Ubuntu 20.04/22.04 2. 拥有NVIDIA显卡(显存≥24G) # 保存两个文件 1 init.sh 初始化 2、test.sh 测试 # init.sh #!/bin/bash # 系统更新与基础依赖sudo apt update && s…...

从软件到硬件:三大主流架构的特点与优劣详解

常见的架构包括软件架构、企业架构、硬件架构等&#xff0c;以下是对这几种常见架构的分析&#xff1a; 一、软件架构 1.分层架构 描述&#xff1a;分层架构是一种经典的软件架构模式&#xff0c;将软件系统按照功能划分为不同的层次&#xff0c;一般包括表现层&#xff08;…...

STM32printf重定向到串口含armcc和gcc两种方案

STM32串口重定向&#xff1a;MDK与GCC环境下需重写的函数差异 在嵌入式开发中&#xff0c;尤其是使用 STM32系列微控制器 的项目中&#xff0c;调试信息的输出是不可或缺的一部分。为了方便调试&#xff0c;开发者通常会选择将 printf 等标准输出函数通过 UART 串口发送到 PC …...