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 不是。

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

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

相關文章

開發搭載OneNet平臺的物聯網數據收發APP的設計與實現

一、開發環境與工具準備 工具安裝 下載HBuilderX開發版(推薦使用開發版以避免插件兼容性問題)安裝Node.js和npm(用于依賴管理及打包)配置Android Studio(本地打包需集成離線SDK)項目初始化 創建uni-app項目,選擇“默認模板”或“空白模板”安裝必要的UI庫(如uView或Van…

HHsuite3 的 HHblits 和 HHsearch比較

HHblits 與 HHsearch 的核心區別及遠源同源檢測能力對比 一、核心功能與定位差異 特征HHblitsHHsearch核心目標快速迭代搜索,構建高質量多序列比對(MSA)和 Profile HMM,用于大規模序列聚類與初步同源篩選。高精度 Profile HMM-HMM 比對,用于深度同源檢測與結構 / 功能預測…

【從零開始學習RabbitMQ | 第二篇】生成交換機到MQ的可靠性保障

目錄 ?編輯前言 交換機 Direct交換機與Fanout交換機的差異 Topic交換機 Topic交換機相比Direct交換機的差異 生成我們的交換機&#xff0c;隊列&#xff0c;以及綁定關系 基于代碼去生成交換機和隊列 基于注解去聲明隊列和交換機 消息轉換器 消息隊列的高可靠性 發送…

LeetCode 熱題 100 22. 括號生成

LeetCode 熱題 100 | 22. 括號生成 大家好&#xff0c;今天我們來解決一道經典的算法題——括號生成。這道題在 LeetCode 上被標記為中等難度&#xff0c;要求生成所有可能的并且有效的括號組合。這是一道非常經典的回溯法題目&#xff0c;非常適合用來練習遞歸和回溯的技巧。…

TestStand API 簡介

TestStand API 簡介 在自動化測試領域&#xff0c;TestStand 憑借其靈活的架構和強大的功能&#xff0c;成為眾多開發者的首選工具。而 TestStand API&#xff08;Application Programming Interface&#xff0c;應用程序編程接口&#xff09;則是打開 TestStand 強大功能的 “…

如何修改 JAR 包中的源碼

如何修改 JAR 包中的源碼 前言一、準備工作二、將 JAR 當作 ZIP 打開并提取三、重寫 Java 類方法 A&#xff1a;直接替換已編譯的 .class方法 B&#xff1a;運行時類路徑優先加載 四、修改 MyBatis&#xff08;或其他&#xff09;XML 資源五、重新打包 JAR&#xff08;命令行&a…

存算一體架構下的新型AI加速范式:從Samsung HBM-PIM看近內存計算趨勢

引言&#xff1a;突破"內存墻"的物理革命 馮諾依曼架構的"存儲-計算分離"設計正面臨根本性挑戰——在GPT-4等萬億參數模型中&#xff0c;數據搬運能耗已達計算本身的200倍。存算一體&#xff08;Processing-In-Memory, PIM&#xff09;技術通過?在存儲介…

藍橋杯15屆國賽 合法密碼

問題描述 小藍正在開發自己的 OJ 網站。他要求網站用戶的密碼必須符合以下條件&#xff1a; 長度大于等于 8 個字符&#xff0c;小于等于 16 個字符。必須包含至少 1 個數字字符和至少 1 個符號字符。 例如 **lanqiao2024!、-*/0601、8((>w<))8** 都是合法的密碼。 而…

Jenkins忘記admin密碼后的恢復步驟

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 前言一、pandas是什么&#xff1f;二、使用步驟 1.引入庫2.讀入數據 總結 前言 提示&#xff1a;這里可以添加本文要記錄的大概內容&#xff1a; 時間較長沒有使用…

C++ - 仿 RabbitMQ 實現消息隊列(1)(環境搭建)

C - 仿 RabbitMQ 實現消息隊列&#xff08;1&#xff09;&#xff08;環境搭建&#xff09; 什么是消息隊列核心特點核心組件工作原理常見消息隊列實現應用場景優缺點 項目配置開發環境技術選型 更換軟件源安裝一些工具安裝epel 軟件源安裝 lrzsz 傳輸工具安裝git安裝 cmake安裝…

簡單面試提問

Nosql非關系型數據庫&#xff1a; Mongodb&#xff1a;開源、json形式儲存、c編寫 Redis&#xff1a;key-value形式儲存&#xff0c;儲存在內存&#xff0c;c編寫 關系型數據庫&#xff1a; sqlite;&#xff1a;輕量型、0配置、磁盤存儲、支持多種語言 mysql&#xff1a;開源…

油氣地震資料信號處理中的NMO(正常時差校正)

油氣地震資料信號處理中的NMO&#xff08;正常時差校正&#xff09;介紹與應用 NMO基本概念 **正常時差校正&#xff08;Normal Moveout Correction&#xff0c;NMO&#xff09;**是地震資料處理中的一項關鍵技術&#xff0c;主要用于消除由于炮檢距&#xff08;source-recei…

深度解析:從 GPT-4o“諂媚”到 Deepseek“物理腔”,透視大模型行為模式的底層邏輯與挑戰

深度解析&#xff1a;從 GPT-4o“諂媚”到 AI“物理腔”&#xff0c;透視大模型行為模式的底層邏輯與挑戰 標簽&#xff1a;人工智能, GPT-4o, 大語言模型, AI倫理, 人機交互, 技術思考 大家好&#xff01;最近AI圈最火的“瓜”之一&#xff0c;莫過于OpenAI的GPT-4o模型在一…

Java引用RabbitMQ快速入門

這里寫目錄 Java發送消息給MQ消費者接收消息實現一個隊列綁定多個消費者消息推送限制 Fanout交換機路由的作用Direct交換機使用案例 Java發送消息給MQ public void testSendMessage() throws IOException, TimeoutException {// 1.建立連接ConnectionFactory factory new Conn…

從讀寫分離到分布式服務:系統架構演進十階段深度解析

第一階段到第四階段&#xff1a;架構進化四階段&#xff1a;探索單體到集群的高可用性能優化之道-CSDN博客https://blog.csdn.net/pinbodeshaonian/article/details/147464084?spm1001.2014.3001.5502 以下是對從第五階段到第十階段詳細的解釋&#xff1a; 第五階段&#xf…

Webug4.0靶場通關筆記07- 第9關反射XSS和第10關存儲XSS

目錄 第09關 反射型XSS 1.打開靶場 2.源碼分析 3.滲透實戰 第10關 存儲型XSS 1.打開靶場 2.源碼分析 3.滲透實戰 本系列為通過《Webug4.0靶場通關筆記》的滲透集合&#xff0c;本文為反射型和存儲型XSS漏洞關卡的滲透部分&#xff0c;通過對XSS關卡源碼的代碼審計找到漏…

Prometheus的安裝部署

目錄 一、概述 二、Prometheus的安裝 1、二進制方式 1.1、下載系統安裝包?編輯 1.2、解壓 1.3、創建數據目錄&#xff0c;服務運行用戶 1.4、設置為系統服務&#xff08;創建服務運行腳本&#xff09; 1.5、啟動服務&#xff0c;并通過瀏覽器訪問驗證 2、容器方式 2…

Jupyter Notebook為什么適合數據分析?

Jupyter Notebook 是一款超實用的 Web 應用程序&#xff0c;在數據科學、編程等諸多領域都發揮著重要作用。它最大的特點就是能讓大家輕松創建和共享文學化程序文檔。這里說的文學化程序文檔&#xff0c;簡單來講&#xff0c;就是把代碼、解釋說明、數學公式以及數據可視化結果…

Python清空Word段落樣式的方法

在 Python 中&#xff0c;你可以使用 python-docx 庫來操作 Word 文檔&#xff0c;包括清空段落樣式。以下是幾種清空段落樣式的方法&#xff1a; 方法一&#xff1a;直接設置段落樣式為"Normal" from docx import Documentdoc Document(your_document.docx) # 打…

macOS 上是否有類似 WinRAR 的壓縮軟件?

對于習慣使用 Windows 的用戶來說&#xff0c;WinRAR 是經典的壓縮/解壓工具&#xff0c;但 macOS 系統原生并不支持 RAR 格式的解壓&#xff0c;更無法直接使用 WinRAR。不過&#xff0c;macOS 平臺上有許多功能相似甚至更強大的替代工具&#xff0c;以下是一些推薦&#xff1…