在这里插入图片描述

前言

在 Kotlin 协程的异步编程世界里,Channel 和 Flow 是处理数据流的重要工具,它们有着不同的设计理念与适用场景。本文将对比二者功能与应用场景,详细讲解 Channel 的使用步骤及注意事项 。

一、Channel 与 Flow 的特性对比

Channel 是协程间进行点对点通信的 “管道”,常用来解决经典的生产/消费问题。Channel 具备以下特效

  • 点对点通信:设计用于协程间直接数据传递,数据 “一对一” 消费,发送后仅能被一个接收方获取 。
  • 生产者-消费者模式:典型的 “管道” 模型,生产者协程发数据,消费者协程收数据,适合任务拆分与协作(如多步骤数据处理,各步骤用协程 + Channel 衔接 )。
  • 即时性:数据发送后立即等待消费,强调 “实时” 通信,像事件驱动场景(按钮点击事件通过 Channel 传递给处理协程 )。
  • 背压(Backpressure): Channel 内部通过同步机制处理生产消费速度差。发送快时,若缓冲区满,发送端挂起;接收慢时,若缓冲区空,接收端挂起,自动平衡数据流转 。

作为对比,再来看看 Flow 的特性

  • 数据流抽象:将异步数据视为 “流”,支持冷流(无订阅不产生数据,如从数据库查询数据的 Flow ,订阅时才执行查询 )和热流(如 SharedFlow,多订阅者共享数据,数据产生与订阅解耦 )。
  • 操作符丰富:提供 map(数据映射 )、filter(数据过滤 )、flatMapConcat(流拼接 )等操作,可灵活转换、组合数据流,适合复杂数据处理场景(如网络请求 + 本地缓存数据的流式整合 )。
  • 多订阅者支持: SharedFlow 可广播数据给多个订阅者,数据 “一对多” 消费,如应用全局状态变化(用户登录状态),多个页面协程订阅 Flow 监听更新 。
对比维度ChannelFlow
通信模式点对点,数据 “一对一” 消费支持 “一对多”(SharedFlow),数据可广播
核心场景协程间任务协作、实时事件传递异步数据流处理、复杂数据转换与多订阅
背压处理依赖 Channel 缓冲区与挂起机制通过操作符(如 buffer )或 Flow 自身设计处理
启动特性无 “懒启动”,发送数据逻辑主动执行冷流默认懒启动,订阅时才触发数据生产
划重点:推与拉的哲学

抛开 SharedFlow 这种一对多不谈。Flow 也可以用作“一对一”通信,此时与 Channel 的主要区别是动作发起方不同:

  • Channel 是将数据从生产者推送给消费者,无论是否有接收方,发送数据的动作已经发生。

  • Flow(尤其是冷流)更像是一种拉取模型—— 收集器在收集数据时会“拉取”数据。如果没有接收方请求,发起方不会生产数据。

很多人在面试中被问到两者区别,回答了一堆技术细节,但是没讲到核心,理解“推与拉”的区别才是核心。

二、如何做技术选型

优先用 Channel 的场景
  • “一对一” 数据传递:网络请求协程(发数据)与 UI 更新协程(收数据)通过 Channel 通信,确保数据有序更新界面 。

  • 串行异步任务:后台任务拆分,多个协程分步处理数据(如 “读取文件 → 解析 → 存储”,每步用 Channel 衔接 )。

  • 事件驱动:处理实时、单次事件(如按钮点击、传感器单次触发 ),Channel 能保证事件 “即发即收”,不重复消费 。

优先用 Flow 的场景
  • 数据流处理:需对异步数据做复杂转换(如网络数据 + 本地缓存数据合并、过滤无效数据 ),Flow 的操作符可简化逻辑 。
  • 多订阅者共享数据:应用全局状态(如用户信息、主题配置 ),用 SharedFlow 广播更新,多个协程订阅同步状态 。
  • 懒加载场景:数据生产耗时(如大文件读取、复杂计算 ),Flow 的冷流特性可延迟执行,避免资源浪费 。

三、Channel 的基本使用步骤

  1. 创建 Channel:根据需求选择 Channel 类型,如创建一个带缓冲的 Channel:
val channel = Channel<Int>(capacity = 10) // 缓冲大小为 10 的 Channel,传输 Int 类型数据
  1. 发送数据(生产端):在协程中通过 send 方法发送数据:
CoroutineScope(Dispatchers.Default).launch {for (i in 1..10) {channel.send(i) // 向 Channel 发送 1 到 10 的整数}channel.close() // 数据发送完毕,关闭 Channel
}
  1. 接收数据(消费端):同样在协程中通过 receive 或 consumeEach 等方式接收数据:
CoroutineScope(Dispatchers.Main).launch {channel.consumeEach { data ->Log.d("ChannelDemo", "接收数据:$data") // 消费 Channel 中的数据,这里打印数据}
}

四、四种不同构建方式

Kotlin 协程提供 4 种 Channel 类型,适配不同需求:

  • Rendezvous/无缓冲:默认值Channel()
  • Buffered/缓冲:Channel(capacity))
  • Conflated/合并:Channel(Channel.CONFLATED)
  • Unlimited/无限制:Channel(Channel.UNLIMITED)
Rendezvous Channel(默认类型)
  • 特性:无缓冲区,发送(send)和接收(receive)需 “同步碰头” 。发送方先调用 send 会挂起,直到接收方调用 receive;反之亦然 。
  • 适用场景:严格同步的协程协作,如 “请求 - 响应” 模式(协程 A 发请求,协程 B 必须接收并响应后,A 才继续执行 )。
val rendezvousChannel = Channel<String>()
// 发送协程
CoroutineScope(Dispatchers.IO).launch {rendezvousChannel.send("无缓冲数据") // 若此时无接收方,发送方会挂起
}
// 接收协程
CoroutineScope(Dispatchers.Main).launch {val data = rendezvousChannel.receive() // 接收数据,发送方恢复Log.d("ChannelDemo", "Rendezvous 接收:$data")
}
Buffered Channel
  • 特性:有固定大小缓冲区,发送方可连续发数据到缓冲区,直到填满;缓冲区满后,发送方挂起。接收方从缓冲区取数据,空了则挂起 。
  • 适用场景:平衡生产消费速度差,如日志收集(生产快,消费慢,缓冲区暂存日志 )。
Conflated Channel
  • 特性:缓冲区大小为 1,新数据覆盖旧数据。发送方发数据时,若缓冲区有数据,直接替换;接收方始终取最新数据 。
  • 适用场景:关注 “最新状态”,如实时传感器数据(只需要当前最新值,旧值无意义 )。
val conflatedChannel = Channel<Int>(Channel.CONFLATED)
// 快速发送多条数据
CoroutineScope(Dispatchers.Default).launch {conflatedChannel.send(1)conflatedChannel.send(2)conflatedChannel.send(3) // 新数据会覆盖旧数据,最终接收方拿到 3
}
// 接收协程
CoroutineScope(Dispatchers.Main).launch {val data = conflatedChannel.receive() Log.d("ChannelDemo", "Conflated 接收:$data") // 输出 3
}
Unlimited Channel
  • 特性:缓冲区无界(理论上可存无限数据 ),发送方不会因缓冲区满挂起,但需注意内存溢出风险(数据生产远快于消费时,内存会持续增长 )。
  • 适用场景:数据量可控,或消费速度能追上生产速度(如固定任务队列,任务数有限 )。实际项目中很少使用,因为经常会造成内存溢出。

五、Channel 实战示例

示例1: 安卓 Snackbar 事件传递(协程间协作)

在安卓开发中,用 Channel 传递 “显示 Snackbar” 事件:

  • 发送端:ViewModel 协程触发事件,通过 Channel 发送消息 。
  • 接收端:Activity/Fragment 协程接收事件,更新 UI 显示 Snackbar 。

优势:解耦事件生产与消费,确保事件 “一对一” 处理,避免重复显示 。

class SnackbarViewModel : ViewModel() {// 声明 Channel,用于传递 Snackbar 消息(String 类型为例)private val _snackbarChannel = Channel<String>()// 暴露为 Flow,方便界面侧收集(也可直接暴露 Channel,但 Flow 更符合 Jetpack 生态)val snackbarFlow = _snackbarChannel.receiveAsFlow()// 触发 Snackbar 事件的方法(可在任意异步逻辑后调用)fun triggerSnackbar(message: String) {viewModelScope.launch {_snackbarChannel.send(message) // 发送事件到 Channel}}
}class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {val viewModel: SnackbarViewModel = viewModel()// 收集 Snackbar 事件流val snackbarMessage by viewModel.snackbarFlow.collectAsState(initial = "")Column {// 模拟触发事件的按钮Button(onClick = {viewModel.triggerSnackbar("操作成功!") // 触发事件}) {Text(text = "显示 Snackbar")}// 根据事件显示 Snackbarif (snackbarMessage.isNotBlank()) {Snackbar(onDismiss = { /* 可在此处理 Snackbar 消失逻辑,比如置空消息 */ }) {Text(text = snackbarMessage)}}}}}
}
  • ViewModel 里用 Channel 作为 “事件管道”,发送端(triggerSnackbar)通过 send 传递消息。
  • 界面侧通过 receiveAsFlow 将 Channel 转为 Flow,用 collectAsState 收集状态,驱动 UI 显示 Snackbar。
  • 因 Channel 是 “一对一” 消费(receiveAsFlow 会按顺序消费事件,且事件被消费后从管道移除 ),可避免重复显示问题。
示例2: 多协程任务拆分(生产者 - 消费者)

处理 “读取文件 → 解析 → 存储” 流程:

  1. 协程 1(生产者):读文件内容,发数据到 Channel 。
  2. 协程 2(消费者):从 Channel 取内容,解析后发新 Channel 。
  3. 协程 3(消费者):从新 Channel 取解析后数据,存入数据库 。

优势:拆分任务到不同协程,利用 Channel 串联流程,实现并行处理(如读文件和解析可部分并行 ),提升效率 。

假设的工具类(模拟文件读取、数据库存储 )

object FileUtils {// 模拟 “读取文件内容”,实际可替换为真实文件 IOsuspend fun readFileContent(filePath: String): String {delay(1000) // 模拟 IO 耗时return File(filePath).readText()}
}object DatabaseUtils {// 模拟 “插入数据库”,实际可替换为 Room 等框架逻辑suspend fun insertIntoDb(data: String) {delay(500) // 模拟数据库操作耗时println("已存入数据库:$data") // 日志演示,实际可省略}
}

主逻辑代码(协程拆分 + Channel 串联 )

fun main() = runBlocking {// 1. 初始化 Channel://    - 第 1 个 Channel:传递原始文件内容(生产者 → 解析协程)val rawDataChannel = Channel<String>()//    - 第 2 个 Channel:传递解析后的数据(解析协程 → 存储协程)val parsedDataChannel = Channel<String>()// 2. 启动 3 个协程,模拟 “生产者 → 消费者 1 → 消费者 2” 流程val producerJob = launch(Dispatchers.IO) {// 生产者:读文件(模拟)val content = FileUtils.readFileContent("/sdcard/sample.txt") rawDataChannel.send(content) // 发送原始内容到 ChannelrawDataChannel.close() // 发送完毕,关闭 Channel}val parserJob = launch(Dispatchers.Default) {// 消费者 1:解析数据for (rawData in rawDataChannel) { // 自动遍历 Channel,直到关闭val parsedData = rawData.replace("\\s+".toRegex(), " ") // 简单解析:去除多余空格parsedDataChannel.send(parsedData) // 发送解析后内容到下一个 Channel}parsedDataChannel.close() // 解析完毕,关闭 Channel}val storageJob = launch(Dispatchers.IO) {// 消费者 2:存储到数据库for (parsedData in parsedDataChannel) { // 自动遍历 Channel,直到关闭DatabaseUtils.insertIntoDb(parsedData)}}// 3. 等待所有任务完成producerJob.join()parserJob.join()storageJob.join()println("所有流程执行完毕!")
}
  • 生产者协程(producerJob):负责 IO 操作(读文件),将结果发送到 rawDataChannel。
  • 解析协程(parserJob):从 rawDataChannel 取数据、解析,再发送到 parsedDataChannel。
  • 存储协程(storageJob):从 parsedDataChannel 取数据、执行数据库插入。
  • 通过 Channel 串联流程,读文件和解析可并行(生产者读文件时,解析协程可能已就绪等待数据 ),提升整体效率;同时代码解耦,每个协程专注单一职责。

六、高级用法:扇入/扇出和双向通信

扇入(Fan-In):

多个发送者,单个接收者。所有协程都对同一个实例调用 channel.send() 并由该单个接收者处理所有消息。这非常适合将来自多个生产者的数据聚合到一个消费者。

val channel = Channel<String>() // 多个生产者
repeat(3) { index -> launch { val producerName = "Producer-$index"repeat(5) { i -> channel.send("$producerName send item$i") } } 
} // 单个消费者
launch { repeat( 15 ) { val item = channel.receive() println( "Consumer received: $item " ) } channel.close() 
}
扇出 (Fan-Out):

单个发送者将数据发送给多个潜在消费者。注意:此时 多个接收者实际上会竞争消息。一个接收者消费的消息不会被另一个接收者看到,即一旦一个数据项被一个消费者读取,它就消失了。如果你希望每个消费者都接收相同的数据,需要使用 SharedFlow

val channel = Channel< Int >() // 单个生产者
launch { repeat(10) { i -> channel.send(i) } channel.close() 
} // 多个消费者
repeat(2) { index -> launch { for (msg in channel) { println( "Receiver-$index receive $msg " ) } } 
}
双向通信

由于 Channel 是单向的,因此有两种主要方式来实现双向通信:

方法1:使用两个独立的 Channel(最简单的方法),一个 Channel 用于 A → B;另一个 Channel 为 B → A。

val channelAtoB = Channel<String>() 
val channelBtoA = Channel<String>() // 协程 A
launch { channelAtoB.send( " Hello from A !" ) val response = channelBtoA.receive() println( "A receive:$response " ) 
} // 协程 B
launch { val msg = channelAtoB.receive() println( "B receive:$msg " ) channelBtoA.send( "Hey A, this is B!" ) 
}

方法2:使用包含结构化消息的单一渠道

  • 定义一个密封类(或其他结构),表明谁发送了它或者它是什么类型的消息。
  • 两个协程都从同一个 Channel 读取,但只响应与它们相关的消息。
seal  class  ChatMessage { data  class  FromA ( val content: String) : ChatMessage() data  class  FromB ( val content: String) : ChatMessage() 
} val chatChannel = Channel<ChatMessage>() // 协程 A
launch { // 发送初始消息chatChannel.send(ChatMessage.FromA( "Hello from A" )) // 在同一 Channel 中等待 B 的响应for (msg in chatChannel) { when (msg) { is ChatMessage.FromB -> { println( "A got B's message: ${msg.content} " ) break} else -> { /* 忽略来自 A 自身的消息 */ } } } 
} // 协程 B
launch { for (msg in chatChannel) { when (msg) { is ChatMessage.FromA -> { println( "B got A's message: ${msg.content} " ) // 在同一 Channel 中响应chatChannel.send(ChatMessage.FromB( "Hey A, this is B!" )) break} else -> { /* 忽略来自 B 的消息 */ } } } chatChannel.close() 
}

方案2 有个风险:如果双方同时等待发送和接收,且没有任何额外的逻辑,则可能会陷入死锁(两个协程都暂停,等待对方读取)。

方案1 两个独立 Channel 通常可以降低这种风险,因为双方都可以发送消息,而无需等待对方从同一 Channel 消费,但是方案2会让代码变得复杂一些。方案各有利有弊,需要开发者自己权衡

七、Channel 异常处理

Channel 通信过程中很容易发生异常,妥善的异常处理非常重要。

使用 try-catch

发送或接收数据时可能出现异常,如 Channel 已关闭还尝试发送。需用 try-catch 包裹关键操作:

一种直接的方法是将发送 / 接收操作包裹在 try-catch 块中:

launch {try {channel.send("Important message")} catch (e: CancellationException) {// 协程被取消,按需处理或记录日志} catch (e: Exception) {// 发送时出现的其他错误}
}

同样的思路也适用于 receive() 调用:

launch {try {val msg = channel.receive()println("Received: $msg")} catch (e: ClosedReceiveChannelException) {// Channel 已关闭} catch (e: Exception) {// 处理其他异常}
}
使用 SupervisorJob

如果我们需要构建一个以协程为主的生产消费系统,可以将它们放在 SupervisorJob 或自定义的 CoroutineExceptionHandler 中,这样可以确保一个失败的协程不搞垮其他协程:

val supervisor = SupervisorJob()
val scope = CoroutineScope(Dispatchers.IO + supervisor + CoroutineExceptionHandler { _, throwable ->// 记录或处理未捕获的异常
})// 然后在这个作用域中启动生产者/消费者协程
出错时及时 close

当 Channel 的某个阶段出现错误时,需要注意关闭 Channel 以表示不会发送任何数据,也有助于通知其他协程停止等待更多数据。

例如:

launch {try {for (line in rawDataChannel) {val cleanedLine = transform(line)processedDataChannel.send(cleanedLine)}} catch (e: Exception) {// 记录错误processedDataChannel.close(e) // 让下游知道发生了故障} finally {processedDataChannel.close()}
}
ClosedSendChannelException

一个常见的错误是忽略这种情况:当发送方处于挂起状态并等待发送时,Channel 可能会关闭。在这种情况下,Kotlin 会抛出 ClosedSendChannelException。我们可以在代码中对这种情况妥善处理,例如重试或者加日志等。

launch {try {channel.send("Data that might fail if channel closes")} catch (e: ClosedSendChannelException) {// Channel 在挂起时被关闭// 决定如何处理或记录这种情况}
}
重试或回退逻辑

有时在向 Channel 发送数据之前,需要重试失败的操作(例如,网络请求)。此时需要一个小循环:

suspend fun safeSendWithRetry(channel: SendChannel<String>, data: String, maxRetries: Int) {var attempts = 0while (attempts < maxRetries) {try {channel.send(data)return} catch (e: Exception) {attempts++if (attempts >= maxRetries) {throw e}delay(1000) // 重试前稍等片刻}}
}

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MYsql主从复制部署

MySQL 主从复制是将主数据库的变更自动同步到从数据库的过程&#xff0c;常用语读写分离、高可用性和数据备份。 1.环境准备 确保主从服务器已安装相同版本的 MySQL&#xff0c;并能通过网络互相访问。 # 检查 MySQL 版本 mysql -V 2.配置主服务器 &#xff08;1&#xff0…

安灯呼叫看板如何实现汽车生产异常秒级响应

在汽车零部件工厂的静置车间&#xff0c;传统生产管理依赖人工巡检与纸质记录&#xff0c;存在效率低、信息滞后、异常响应慢等问题。某汽车厂曾因物料静置时间未及时监控&#xff0c;导致批次混料&#xff0c;损失超10万元。而安灯呼叫看板系统的引入&#xff0c;通过实时状态…

构造函数注入在spring boot 中怎么使用详解

我们来详细讲解一下在 Spring Boot 中如何使用构造函数注入&#xff0c;并通过一个完整的、可运行的例子来演示。 构造函数注入是 Spring 官方最推荐的依赖注入方式&#xff0c;因为它能保证对象的不可变性和依赖的完整性。 核心理念 在 Spring Boot 中使用构造函数注入非常简单…

2025.6.30-2025.7.06第26周:第一次参加头马演讲俱乐部

现在是周一早上6:23&#xff0c;我开始写上周的周总结。 3件超出预期的事 参加头马俱乐部绝对是最超出预期的&#xff0c;使得这个周末格外的快乐简历的第一版终于改完了&#xff0c;花了好长的时间&#xff0c;其中有一天心情还很荡&#xff0c;因为&#xff0c;我想&#x…

2025使用VM虚拟机安装配置Macos苹果系统下Flutter开发环境保姆级教程--下篇

其实如何安装VM,如何安装MACOS网上的教程很多,我只是结合我的体验重新整理了一次,接下来才进入本教程最核心的部分,Flutter开发环境的配置部分。、一.配置前准备 主要是准备相应的工具包,以及其他虚拟机设置1.工具包 工具包的版本也可以自行配置,我这主要是我使用的是F…

QSPI、OSPI与FSMC的区别与内存映射分析

QSPI、OSPI与FSMC的区别与内存映射分析 基本概念与区别 1. FSMC (灵活静态存储控制器) 接口类型&#xff1a;并行接口&#xff0c;通常8/16位数据总线总线标准&#xff1a;传统并行总线协议速度&#xff1a;相对较低&#xff0c;通常最高约100MHz应用场景&#xff1a;SRAM、NOR…

系统思考与心智模式探索

成长的真正障碍&#xff0c;不是能力的不足&#xff0c;而是看待问题的局限。 在复杂多变的商业环境中&#xff0c;我们往往习惯于解决“眼前”的问题&#xff0c;却忽视了深藏背后的系统性障碍。我们看到的只是表面的“症状”&#xff0c;而真正的根源&#xff0c;却往往隐藏…

物联网技术的关键技术与区块链发展趋势的深度融合分析

一、物联网技术的核心架构与关键技术 物联网技术体系由感知层、网络层、平台层、应用层和安全层构成&#xff0c;各层技术协同工作&#xff0c;实现物理世界与数字世界的深度融合。 感知层&#xff1a;物联网的“感官” 传感器技术&#xff1a;包括环境传感器&#xff08;温度…

针对Exhcnage Server的攻击防范措施

一、背景介绍最近&#xff0c;安全研究人员揭露了一个名为 NightEagle&#xff08;又名 APT-Q-95&#xff09; 的高级持续性威胁&#xff08;APT&#xff09;组织。这个组织被观察到利用 Microsoft Exchange 服务器中的零日漏洞链 进行攻击&#xff0c;其主要目标是中国政府、国…

编程基础:继承

能帮到你的话&#xff0c;就给个赞吧 &#x1f618; 文章目录继承&#xff1a;使用基类成员&#xff1a;前提——派生类必须同样支持基类成员(组件和功能)示例&#xff1a;动物Animal 有 鳃 和 会飞。则 鸟Bird 不是 Animal&#xff0c;因为Bird虽会飞&#xff0c;却没有鳃。鱼…

TMC4361A 使用(未验证)

prompt 我用STM32F103C8T6 来控制 TMC4361A 运动控制芯片 &#xff0c;我配置 STM32F103C8T6 的 SPI1 与 TMC4361A 进行通信&#xff0c;配置 PA4 作为片选线&#xff0c;配置 PA8 作为 RCC_MCO 输入时钟输入到 TMC4361A, 并将其连接到TMC4361A的CLK_EXT引脚。我想控制 TMC4361…

深度剖析:如何解决Node.js中mysqld_stmt_execute参数错误

在Node.js后端开发中&#xff0c;使用mysql2等数据库驱动与MySQL/MariaDB交互时&#xff0c;Incorrect arguments to mysqld_stmt_execute 是一个令人头疼的错误。它通常意味着你传递给SQL预处理语句的参数数量与SQL字符串中问号&#xff08;?&#xff09;占位符的数量不匹配。…

Vue3 学习教程,从入门到精通,Vue 3 安装指南及语法知识点详解(2)

Vue 3 安装指南及语法知识点详解 本文将详细介绍 Vue 3 的所有安装方式&#xff0c;并深入讲解 Vue 3 的语法知识点。此外&#xff0c;还将提供一些综合性案例&#xff0c;展示如何综合运用 Vue 3 的各项功能。一、安装 Vue 3 的所有方式 Vue 3 提供了多种安装方式&#xff0c;…

C++基础复习笔记

一、数组定义 在C中&#xff0c;数组初始化有多种方式&#xff0c;以下是常见的几种方法&#xff1a; 默认初始化 数组元素未显式初始化时&#xff0c;内置类型&#xff08;如int、float&#xff09;的元素值未定义&#xff08;垃圾值&#xff09;&#xff0c;类类型调用默认构…

手机和PC远控安全深度测评:TeamViewer/ToDesk/向日葵安全防线对比

声明&#xff1a;本测试报告系作者基于个人兴趣及使用场景开展的非专业测评&#xff0c;测试过程中所涉及的方法、数据及结论均为个人观点&#xff0c;不代表任何官方立场或行业标准。 一、引言 当下远程控制技术已深度融入大众的工作与生活&#xff0c;无论是上班族在家操…

Windows 11的开始菜单调整为左下角布局

1.桌面右键个性化 2.个性化中任务栏 3.任务栏选择任务栏行为 4.任务栏行为中 任务栏对齐方式选择靠左即可

Go语言项目工程化 — 常见开发工具与 CI/CD 支持

在Go语言的项目工程化实践中&#xff0c;常见开发工具与 CI/CD 支持是保障团队协作、高效交付与项目质量的关键。以下是第 68 章的详细内容。一、开发辅助工具Go语言生态为开发者提供了丰富的工具&#xff0c;以提高代码质量与开发效率。1. 格式化与静态检查工具说明gofmt标准格…

OpenCV人脸分析------绘制面部关键点函数drawFacemarks()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 该函数用于在图像上绘制面部关键点&#xff08;facial landmarks&#xff09;&#xff0c;例如使用 FacemarkLBF, FacemarkKazemi 等算法检测到的…

Linux内核ext4 extent:解决大文件存储难题的关键

在Linux 操作系统的庞大生态中&#xff0c;文件系统犹如一座城市的基础设施&#xff0c;支撑着数据的有序存储与高效访问。而 ext4 文件系统&#xff0c;作为 Linux 文件系统家族中的重要一员&#xff0c;自诞生起便凭借诸多先进特性备受瞩目。其中&#xff0c;extent 机制堪称…

reactnative页面适配UI设计尺寸px转dp的完美解决方案px2dp精要篇

你的 px2dp 函数基本思路是正确的&#xff0c;但可以进一步优化以确保更精确的适配。以下是改进后的完美精确方案&#xff1a; 完美精确的适配方案 import { Dimensions, PixelRatio, Platform, ScaledSize } from react-native;// 获取屏幕尺寸&#xff08;考虑横竖屏&#…