Kotlin 協程之 Flow 的理解使用及源碼解析

在這里插入圖片描述

前言

在前面的文章中,我們已經討論了 Channel 的概念和基本使用以及 Channel 的高階應用。這篇我們來看日常開發中更常用的Flow


“冷流” 和 “熱流” 的本質

先來梳理一下所謂的 “冷流” 和 “熱流”。

核心概念

我們已經知道 Channel 是 “熱流”,而用 flow{} 構建器創建的 Flow 是 “冷流”(Flow 也有熱流形式,如 SharedFlow
StateFlow,這個后面文章會詳細介紹)。

所謂的熱和冷,本質上是指"數據生產"和"數據消費"是否解耦為兩套獨立邏輯。

  • 熱流:不依賴消費端,提前生產數據,即使沒有消費者也會持續工作

  • 冷流:當有消費端觸發消費事件時,才開始生產數據,懶加載的思想

對比示例

讓我們通過代碼來直觀感受一下:

Channel(熱流)示例

suspend fun channelHotExample() {val channel = Channel<String>(Channel.BUFFERED)// 生產者開始工作,不管有沒有消費者val producer = GlobalScope.launch {repeat(3) { i ->delay(1000)channel.send("熱流數據-$i")println("\u001B[32m[Channel生產者] 數據已發送: 熱流數據-$i\u001B[0m")}channel.close()}// 等待2秒后才開始消費delay(2500)println("\u001B[34m[Channel消費者] 2秒后開始消費...\u001B[0m")for (data in channel) {println("\u001B[34m[Channel消費者] 收到: $data\u001B[0m")}producer.join()
}

在這里插入圖片描述

Flow(冷流)示例

Flow 的創建和收集很簡單:用 flow {} 創建,用 collect 收集。

suspend fun flowColdExample() {// 定義Flow,但此時還沒有開始生產數據val flow = flow {repeat(3) { i ->delay(1000)println("\u001B[32m[Flow生產者] 數據開始發送: 冷流數據-$i\u001B[0m")emit("冷流數據-$i")println("\u001B[32m[Flow生產者] 數據發送完畢: 冷流數據-$i\u001B[0m")println()}}println("\u001B[36mFlow已定義,但還沒有開始生產數據\u001B[0m")println()delay(2000)println("\u001B[34m[Flow消費者] 開始收集,此時才開始生產數據...\u001B[0m")flow.collect { data ->println("\u001B[34m[Flow消費者] 收到: $data\u001B[0m")// 模擬處理邏輯delay(500)println("\u001B[34m[Flow消費者] 數據處理完畢: $data...\u001B[0m")}
}

熱流 vs 冷流對比

特性熱流冷流
數據生產立即開始,不管有沒有消費者只有在被收集時才開始生產
數據共享多個消費者共享同一份數據每個收集器都有獨立的數據流
資源消耗持續消耗資源按需消耗資源
背壓處理通過緩沖區和掛起機制天然支持背壓,生產速度跟隨消費
生命周期獨立于消費者與收集器生命周期綁定
內存使用需要緩沖區存儲數據按需生產,內存友好

Flow 的基本使用

Flow 的創建有多種方式,不同方式背后的實現原理和適用場景也不同。

flow 構建器

flow{} 構建器是最常用的也是很重要的一種方式。后面提到的 flowOfasFlow、以及 channelFlow,本質上都是對 flow{}
的封裝或擴展。

使用示例

先來看個最簡單的例子:

suspend fun basicFlowBuilder() {val numberFlow = flow {repeat(5) { i ->delay(500)emit(i) // 發射數據}}numberFlow.collect { value ->println("[消費者] 收到: $value")}
}

使用起來非常簡單,用 flow{} 創建,用 collect 收集即可。

flowOf

當你有一組已知的靜態數據需要轉成 Flow,flowOf 最方便:

suspend fun flowOfExample() {val staticFlow = flowOf("Apple", "Banana", "Cherry")staticFlow.collect { fruit ->println("[消費者] 水果: $fruit")}}

源碼

// Flow.kt
public fun <T> flowOf(vararg elements: T): Flow<T> = flow {for (element in elements) {emit(element)}
}

可以看到,flowOf 本質就是對 flow 的封裝:遍歷所有元素并逐個 emit

asFlow 擴展函數

將各類數據結構直接轉換為 Flow

在這里插入圖片描述

示例:

suspend fun asFlowExample() {
// 集合轉Flowval listFlow = listOf(1, 2, 3, 4, 5).asFlow()listFlow.collect { number ->println("\u001B[34m[消費者] 數字: $number\u001B[0m")}// 區間轉Flowval rangeFlow = (1..3).asFlow()rangeFlow.collect { value ->println("\u001B[34m[消費者] 區間值: $value\u001B[0m")}// 數組轉Flowval arrayFlow = arrayOf("A", "B", "C").asFlow()arrayFlow.collect { letter ->println("\u001B[34m[消費者] 字母: $letter\u001B[0m")}}

在這里插入圖片描述

源碼


// Iterable -> Flow
public fun <T> Iterable<T>.asFlow(): Flow<T> = flow {forEach { value ->emit(value)}
}// Array -> Flow
public fun <T> Array<T>.asFlow(): Flow<T> = flow {forEach { value ->emit(value)}
}

核心特點:同步轉換、按需發射、內存友好。


Flow 源碼分析

Flow 內部的實現還是很有意思的, 我們就基于上述示例代碼結合源碼進行分析,來看一看 flow 內部的執行流程。

1. 創建階段

例如:


val numberFlow = flow {repeat(5) { i ->delay(500)emit(i)}
}
flow{}
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)
  • 參數類型suspend FlowCollector<T>.() -> Unit(一個可掛起的擴展函數類型)

  • 返回值:返回 SafeFlow

FlowCollector
public fun interface FlowCollector<in T> {/*** Collects the value emitted by the upstream.* This method is not thread-safe and should not be invoked concurrently.*/public suspend fun emit(value: T)
}

可以看到,FlowCollector 接口提供了 emit 方法,因此我們才能在 flow{} 代碼塊里直接調用 emit(),因為這個代碼塊本身就是FlowCollector 的擴展函數。

SafeFlow

private class SafeFlow<T>(private val block: suspend FlowCollector<T>.() -> Unit) : AbstractFlow<T>() {override suspend fun collectSafely(collector: FlowCollector<T>) {collector.block()//這里的 block,實際上就是我們寫的生產邏輯的代碼塊}
}

關鍵點:

  1. flow { ... } 返回 SafeFlow 實例
  2. 傳入的 block 實際就是我們寫的生產邏輯代碼塊
  3. 此時生產邏輯并沒有被執行,等待 collectSafely 被調用后才會執行生產邏輯

2. 收集階段

numberFlow.collect { value ->println("[消費者] 收到: $value")
}

numberFlowSafeFlow 的實例

AbstractFlow.collect()

SafeFlow 繼承自 AbstractFlow,當我們調用 numberFlow.collect 時,實際上是走到了 AbstractFlow.collect()

// kotlinx-coroutines-core/common/src/flow/AbstractFlow.kt
public abstract class AbstractFlow<T> : Flow<T>, CancellableFlow<T> {/*** 這里的 collector,就是我們寫的消費邏輯的代碼塊*/public final override suspend fun collect(collector: FlowCollector<T>) {// 創建 SafeCollector ,并且把消費邏輯代碼塊作為參數傳遞進去val safeCollector = SafeCollector(collector, coroutineContext)try {//調用實現類(SafeFlow)的collectSafely,此時的safeCollector是包含了消費邏輯代碼塊的collectSafely(safeCollector)} finally {safeCollector.releaseIntercepted()}}
}

關鍵點:

  • safeCollector 創建時,把 collector 傳入,并且把 collector 包裝成 SafeCollector,也就是保存了消費邏輯代碼塊
  • 然后,調用了 collectSafely,這里會走到實現類(SafeFlow)的collectSafely

SafeFlow.collectSafely()

//這里的 block 參數上面說過了,是我們生產邏輯的代碼塊
private class SafeFlow<T>(private val block: suspend FlowCollector<T>.() -> Unit) : AbstractFlow<T>() {//這里的collector,則是把消費邏輯包裝后的SafeCollector,當觸發 collect 后,會觸發生產邏輯,此時,collector也是包含消費邏輯了的。override suspend fun collectSafely(collector: FlowCollector<T>) {collector.block()}
}

關鍵點:

  • collector.block() 中的 collectorSafeCollector 實例

  • block 是我們寫的生產邏輯(flow { ... } 中的代碼),也就是示例中的:

repeat(5) { i ->delay(500)emit(i)
}

當執行到 emit(i) 時,實際上調用的是 SafeCollector.emit(i)

SafeCollector.emit()


internal actual class SafeCollector<T> actual constructor(@JvmField internal actual val collector: FlowCollector<T>,//保存的消費代碼塊@JvmField internal actual val collectContext: CoroutineContext
) : FlowCollector<T>, ContinuationImpl(NoOpContinuation, EmptyCoroutineContext), CoroutineStackFrame {/*** This is a crafty implementation of state-machine reusing.* First it checks that it is not used concurrently (which we explicitly prohibit) and* then just cache an instance of the completion in order to avoid extra allocation on each emit,* making it effectively garbage-free on its hot-path.*/override suspend fun emit(value: T) {return suspendCoroutineUninterceptedOrReturn sc@{ uCont ->try {emit(uCont, value)//執行下面的私有方法} catch (e: Throwable) {// Save the fact that exception from emit (or even check context) has been thrown// Note, that this can the first emit and lastEmissionContext may not be saved yet,// hence we use `uCont.context` here.lastEmissionContext = DownstreamExceptionContext(e, uCont.context)throw e}}}private fun emit(uCont: Continuation<Unit>, value: T): Any? {val currentContext = uCont.contextcurrentContext.ensureActive()//檢查協程是否處于活躍狀態// This check is triggered once per flow on happy path.//上下文檢查,flow 不允許跨協程發送數據,這個后面會講到val previousContext = lastEmissionContextif (previousContext !== currentContext) {checkContext(currentContext, previousContext, value)lastEmissionContext = currentContext}completion = uCont//真正執行消費的地方:collector 是我們寫的消費邏輯代碼塊,value 就是 生產邏輯中發送的數據val result = emitFun(collector as FlowCollector<Any?>, value, this as Continuation<Unit>)/** If the callee hasn't suspended, that means that it won't (it's forbidden) call 'resumeWith` (-> `invokeSuspend`)* and we don't have to retain a strong reference to it to avoid memory leaks.*/if (result != COROUTINE_SUSPENDED) {completion = null}return result}
}

關鍵點:

  • SafeCollector 保存了消費邏輯代碼塊,也就是參數 collector
  • emit 最終會調用到 emitFun
  • emitFun(collector, value, this) 中的 collector 是我們傳入的消費邏輯代碼塊,就是示例代碼中的
    { value -> println("[消費者] 收到: $value") }

emitFun

@Suppress("UNCHECKED_CAST")
private val emitFun =FlowCollector<Any?>::emit as Function3<FlowCollector<Any?>, Any?, Continuation<Unit>, Any?>

這里要轉換來看

// emitFun 的類型是 Function3<FlowCollector<Any?>, Any?, Continuation<Unit>, Any?>
// 相當于下面代碼:
fun emitFun(collector: FlowCollector<Any?>, value: Any?, completion: Continuation<Unit>): Any? {return collector.emit(value, completion)  // 調用消費者的 emit 方法
} 

當執行 emitFun(collector, value, this) 時:

  • collector 是我們的消費邏輯({ value -> println("收到: $value") }

  • value 是數據(比如 0, 1, 2)

  • this 是 SafeCollector 自己作為 Continuation

我們的消費邏輯代碼塊實際上就是一個 FlowCollector 的實現

// 當我們寫 collect { value -> println("收到: $value") } 時
// 實際上創建了一個匿名類,實現了 FlowCollector 接口:val consumer = object : FlowCollector<Int> {override suspend fun emit(value: Int) {println("收到: $value")  // 我們的消費邏輯}
}

所以當調用 emitFun(collector, value, completion) 時,實際上是在調用我們的消費邏輯

換句話說,當我們調用 emit 時,相當于是把 emit() 替換為消費代碼塊里的代碼。

例如:

suspend fun basicFlowBuilder() {val numberFlow = flow {repeat(5) { i ->delay(500)emit(i) // 發射數據}}numberFlow.collect { value ->println("[消費者] 收到: $value")}
}

就相當于:

suspend fun basicFlowBuilder() {repeat(5) { i ->delay(500)println("[消費者] 收到: $value")//消費代碼塊替換掉 emit}
}

整體執行流程

1.numberFlow.collect { value -> println("[消費者] 收到: $value") }2.AbstractFlow.collect(collector) 被調用-collector = { value -> println("[消費者] 收到: $value") }(我們的消費邏輯)3.SafeCollector(collector, coroutineContext) 被創建-SafeCollector.collector = 我們的消費邏輯
-SafeCollector 自己也實現了 FlowCollector<T>4.SafeFlow.collectSafely(safeCollector) 被調用-safeCollector 是 SafeCollector 實例
↓
5.safeCollector.block() 執行-block 是我們的生產邏輯 : repeat (5) { i -> delay(500); emit(i) }
-當執行到 emit (i) 時 , 調用 SafeCollector.emit(i)6.SafeCollector.emit(i) 執行-檢查上下文一致性
-調用 emitFun (SafeCollector.collector, i, continuation)
-SafeCollector.collector 就是我們的消費邏輯
↓
7.我們的消費邏輯被執行:println("[消費者] 收到: $i")8.繼續下一次循環,直到 repeat(5) 完成

總結

  • SafeFlow 負責執行生產邏輯,SafeCollector 負責執行消費邏輯。

  • collector.block() 中的 collectorSafeCollector 實例,保存了消費邏輯代碼塊

  • block 是我們的生產邏輯(flow { ... } 中的代碼)

  • 當生產邏輯調用 emit(value) 時,會觸發 SafeCollector.emit(value)

  • SafeCollector.emit(value) 最終會調用我們傳入的消費邏輯

以上就是 Flow 內部的實現機制。這就是為什么說 Flow 是"冷流",因為生產邏輯只有在被收集時才開始執行,而且每次收集都是全新的執行。

到這里,Flow 內部的執行機制就搞明白了。


Flow 的重要限制

在看其他創建方式之前,先明確一個非常重要的限制:Flow 不允許在不同協程中調用 emit()

示例

suspend fun wrongConcurrentEmitExample() {val errorFlow = flow {println("\u001B[32m[主協程] 開始創建Flow\u001B[0m")//在新協程中調用emitlaunch {delay(1000)emit("來自子協程的數據")  // 這里會拋出異常!}emit("正常數據")  // 這個是正常的}try {errorFlow.collect { data ->println("\u001B[34m[消費者] 收到: $data\u001B[0m")}} catch (e: Exception) {println("\u001B[31m[異常] ${e.message}\u001B[0m")}
}

在這里插入圖片描述

拋出的異常表明:FlowCollector 不是線程安全的,禁止并發發送數據

報錯堆棧:

在這里插入圖片描述

SafeCollector 的檢查機制

collect 內部會構建 SafeCollector,這個我們前面分析源碼的時候已經知道了

在這里插入圖片描述

SafeCollector 在執行 emit 時會檢查上下文:

在這里插入圖片描述

當協程上下文不一致時,會拋出異常:

在這里插入圖片描述

關鍵代碼:

if (emissionParentJob !== collectJob) {error("Flow invariant is violated:\n" +"\t\tEmission from another coroutine is detected.\n" +"\t\tChild of $emissionParentJob, expected child of $collectJob.\n" +"\t\tFlowCollector is not thread-safe and concurrent emissions are prohibited.\n" +"\t\tTo mitigate this restriction please use 'channelFlow' builder instead of 'flow'")
}

關鍵點emit 發生時的 Job 必須與 collectJob 一致(或為其子層級)。因此在另一協程中調用 emit 會觸發異常。

為什么要有這個限制?

根因在于 Flow 的冷流本質

冷流的設計理念

Flow 是冷流,每次 collect 都會重新執行 flow{} 代碼塊,就像一次函數調用:

  • 調用者(collect)與被調用者(flow{})在同一執行上下文中

  • 生產與消費是同步協作,而非并發競爭

  • 過程需要順序、可預測

協程上下文的一致性

Flow 中,emit 的協程上下文應當與 collect 保持一致。否則:

  • 上下文一致性被破壞,可能產生線程切換問題

  • 異常無法被正確傳播到消費者

  • 取消機制失效,子協程取消無法正確傳遞

源碼分析的時候也提到過,emit() 的代碼相當于是把消費邏輯代碼塊給替換掉 emit(),如果說 Flow
生產邏輯可以跨協程并發執行,那么,消費邏輯邏輯代碼塊就會出現跟預期不符的邏輯。

例如,我原本在消費邏輯默認是在主線程中運行,如果可以跨協程 emit,例如,切到了 IO 線程,那么,消費邏輯就會跑到 emit
所在的IO線程中執行,無法保證上下文一致。


總結

Flow 在使用上還是很簡單的,關鍵是要搞清楚概念特性以及底層實現原理,以及結合特性用在合適的場景中去。

核心原則:Flow 是用來處理數據流的,如果你的場景是一次性的數據獲取,直接用 suspend fun 就夠了。記住,合適的工具做合適的事情,這樣代碼才會既清晰又高效。

Flow 雖然在生產端存在限制,不能跨協程并發地生產數據,但 Kotlin 還給我們提供了其他的解決方案,具備更加靈活的生產端。下一篇文章,我們將深入探討如何突破Flow 的限制,看看 ChannelFlow 的結合之道。


好了, 本篇文章就是這些,希望能幫到你。下一篇:突破 Flow 限制:Channel 與 Flow 的結合之道

感謝閱讀,如果對你有幫助請三連(點贊、收藏、加關注)支持。有任何疑問或建議,歡迎在評論區留言討論。如需轉載,請注明出處:喻志強的博客

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/98746.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/98746.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/98746.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

簡述ajax、node.js、webpack、git

本系列可作為前端學習系列的筆記&#xff0c;HTML、CSS和JavaScript系列文章 已經收錄在前端專欄&#xff0c;有需要的寶寶們可以點擊前端專欄查看&#xff01; 點贊關注不迷路&#xff01;您的點贊、關注和收藏是對小編最大的支持和鼓勵&#xff01; 系列文章目錄 簡述ajax、…

經營幫會員經營:全方位助力企業高效發展,解鎖商業新可能

在商業競爭愈發激烈的當下&#xff0c;企業若想脫穎而出&#xff0c;高效的經營管理體系至關重要。經營幫的會員經營板塊&#xff0c;憑借豐富且實用的功能&#xff0c;為企業打造了一站式的經營助力平臺&#xff0c;從多維度賦能企業&#xff0c;讓發展之路更順暢。會員經營與…

Vue 封裝Input組件 雙向通信

子組件<template><div class"box"><div class"box-left"><input blur"handleBlur" v-model"localInput" class"box-left-input"> </div><div class"box-right"><p style…

伽馬(gamma)變換記錄

此只記錄伽馬變換原理及其應用結果&#xff08;文章所有內容基于數字圖像處理-岡薩雷斯&#xff09;&#xff0c;和直接用MATLAB代碼生成伽馬變換代碼。一、原理伽馬變換的公式很簡答 就是一個有規律的冪運算 公式如下&#xff1a;一般在圖像中進行應用是 C1 y為不同值時r的輸…

電路學習(六)三極管

三極管是一種電流驅動元器件&#xff08;MOS管為電壓驅動&#xff09;&#xff0c;在電路中可以充當開關&#xff0c;放大電流等作用。本文章參考了尚硅谷的視頻資料。1. 什么是三極管&#xff1f;三極管又被稱為晶體三極管&#xff08;Bipolar Junction Transistor&#xff0c…

配置docker常見問題

輸入sudo yum install -y yum-utils device-mapper-persistent-data lvm2出現Cannot find a valid baseurl for repo: base/7/x86_64一、檢查網絡輸入ping www.baidu.com出現PING www.a.shifen.com (220.181.111.1) 56(84) bytes of data. 64 bytes from 220.181.111.1 (220.18…

Python 實戰:票據圖像自動矯正技術拆解與落地教程

在日常辦公自動化&#xff08;OA&#xff09;或財務數字化場景中&#xff0c;拍攝的票據常因角度問題出現傾斜、變形&#xff0c;不僅影響視覺呈現&#xff0c;更會導致 OCR 文字識別準確率大幅下降。本文將從技術原理到代碼實現&#xff0c;手把手教你用 Python 打造票據圖像自…

vue3+TS項目配置unocss

配置unocss &#xff08;1&#xff09;安裝依賴 npm i unocss unocss/preset-uno unocss/preset-attributify -D npm install unocss/transformer-directives&#xff08;2&#xff09;根目錄新建uno.config.ts文件 import { defineConfig } from "unocss"; impor…

嵌入式硬件工程師的每日提問

一、LDO與DC-DC的對比1&#xff09;同&#xff1a;兩者都是將不穩定的直流輸入電壓轉換為穩定的直流輸出電壓。2&#xff09;異&#xff1a;LDO&#xff1a;線性調節&#xff0c;通過內部功率晶體管&#xff0c;工作在線性區&#xff0c;穩定輸出電壓。類比&#xff1a;將湍急的…

從零到一使用Linux+Nginx+MySQL+PHP搭建的Web網站服務器架構環境——LNMP(下)

從零到一使用LinuxNginxMySQLPHP搭建的Web網站服務器架構環境——LNMP&#xff08;上&#xff09;https://coffeemilk.blog.csdn.net/article/details/151350565 一、Nginx與PHP-FPM整合原理 1.1、PHP-FPM配置文件 Nginx與PHP-FPM整合原理序號說明1 PHP-FPM是一個第三方的Fast…

論文閱讀-Correlate and Excite

文章目錄1 背景2 創新點3 方法3.1 總體結構3.2 代價體計算3.3 引導式代價體激勵&#xff08;GCE&#xff09;3.4 TopK視差回歸4 效果參考資料1 背景 在IGEV中構建幾何編碼體CGC_GCG?時用到了本文將要描述的CoEx&#xff0c;IGEV中沒有說明為什么要這樣做&#xff0c;本文就是…

探索大語言模型(LLM):Open-WebUI的安裝

前言 Open-WebUI 是一款專為大模型設計的開源可視化交互工具&#xff0c;它通過類 ChatGPT 的直觀界面&#xff0c;讓用戶無需代碼即可管理、調試和調用本地或云端的大語言模型&#xff08;LLMs&#xff09;&#xff0c;成為私有化部署的便捷工具&#xff0c;本文將介紹如何部…

企業遠程訪問方案選擇:何時選內網穿透,何時需要反向代理?

企業遠程訪問需求日益增長&#xff0c;無論是遠程辦公、分支互聯還是服務發布&#xff0c;選擇合適的網絡方案都至關重要。內網穿透和反向代理是兩種常見的技術手段&#xff0c;但它們的設計目標和適用場景截然不同。本文將客觀分析兩者的特點&#xff0c;幫助企業做出更合理的…

ARM指令集(Instruction Set)細節

ARM指令集(Instruction Set)細節 本文旨在深入探討 ARM 指令集(Instruction Set)的細節。這是一個非常廣泛的主題&#xff0c;我會將其分解為關鍵概念、不同版本的區別以及核心特性&#xff0c;并提供一些示例。 ARM 指令集的核心在于 RISC&#xff08;精簡指令集計算機&#x…

Vue基礎知識-Vue集成 Element UI全量引入與按需引入

一、方式一&#xff1a;全量引入 Element UI全量引入即一次性加載 Element UI 所有組件和樣式&#xff0c;優點是配置簡單&#xff0c;適合快速開發&#xff1b;缺點是打包體積較大&#xff0c;生產環境可能存在冗余。1. 安裝 Element UI全量引入只需安裝 Element UI 核心依賴&…

leetcode26(字母異位詞分組)

給你一個字符串數組&#xff0c;請你將 字母異位詞 組合在一起。可以按任意順序返回結果列表。示例 1:輸入: strs ["eat", "tea", "tan", "ate", "nat", "bat"]輸出: [["bat"],["nat","…

光平面標定 (Laser Plane Calibration) 的原理和流程

光平面標定 (Laser Plane Calibration) 是線激光3D相機系統中最為關鍵且精巧的一步,它直接決定了最終的測量精度。 核心目標 光平面標定的目標是:精確地求出激光器發射出的那個扇形激光平面,在相機坐標系下的數學方程。 這個方程通常表示為一般式: Ax + By + Cz + D = 0…

項目1——單片機程序審查,控制系統項目評估總結報告

執行摘要 本報告對基于STM32F103RET6的老虎機控制系統進行了全面的技術評估。通過深入分析代碼結構、系統架構、安全機制和潛在風險&#xff0c;為項目的進一步開發和部署提供專業建議。 核心發現 ? 系統架構: 設計合理&#xff0c;模塊化程度高?? 安全性: 存在輸入驗證和并…

【Qt應用程序】

Qt應用程序摘要概述快速開始Qt在線下載與安裝Visual Studio開發Qt項目VS配置Qt擴展VS創建Qt項目配置qDebug調試信息配置源程序的字符集項目結構對象樹與內存回收基礎數據類型信號槽定時器窗口QWidgetQMainWindowQDialog窗口布局窗口中添加右鍵菜單控件按鈕類容器類自定義控件事…

機器學習實戰(一): 什么是機器學習

機器學習&#xff1a;讓機器學會思考的魔法前言 在當今數字化的浪潮中&#xff0c;人工智能無疑是最引人注目的技術之一&#xff0c;而機器學習正是其核心驅動力。它不再是科幻電影中的遙遠設想&#xff0c;而是已經滲透到我們日常生活的方方面面&#xff0c;從智能推薦到自動駕…