Kotlin協程在android中的使用總結

認識協程

引用官方的一段話

協程通過將復雜性放入庫來簡化異步編程。程序的邏輯可以在協程中順序地表達,而底層庫會為我們解決其異步性。該庫可以將用戶代碼的相關部分包裝為回調、訂閱相關事件、在不同線程(甚至不同機器!)上調度執行,而代碼則保持如同順序執行一樣簡單。
協程是一種并發設計模式,您可以在Android平臺上使用它來簡化異步執行的代碼

簡單概括:以同步的方式去編寫異步執行的代碼。協程是依賴于線程,但是協程掛起時不需要阻塞線程,幾乎是無代價的。

協程的實現,會用到線程,但是使用協程不用類比線程,跟線程是不同的概念。

Android項目引入協程

  1. 在項目 根build.gradle - buildscript - dependencies下引入kotlin
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  1. 在項目各module - build.gradle - dependencies 中引入kotlin庫和協程庫
//kotlin庫
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
//協程核心庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
//協程android支持庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"

做完以上步驟,就可以在項目中使用協程了。

協程的基礎用法

override fun onCreate(savedInstanceState: Bundle?) {//do someThinglifecycleScope.launch {//非及時任務延后處理delay(2000)getHenanRegion()getCommentHintText()}
}

上面就是,kotlin的簡單使用,示例中延遲了2s,處理非及時響應任務,沒有阻塞主線程。而且沒有借助Handler, 線程.

協程,離不開以下部分

  • CoroutineScope(協程作用域),如示例的lifecycleScope
  • 啟動函數(協程作用域的擴展函數),如示例launch .
  • 掛起函數,一般是網絡請求耗時操作,或者延時功能性函數 如示例中delay(2000) 負責延時2s, 但不阻塞主線程。

明白這些內容就可以寫協程代碼了。

協程作用域

協程作用域(Coroutine Scope)是協程運行的作用范圍。CoroutineScope定義了新啟動的協程作用范圍,同時會繼承了他的coroutineContext自動傳播其所有的 elements和取消操作。換句話說,如果這個作用域銷毀了,那么里面的協程也隨之失效。

協程的啟動函數

示例使用了launch函數,即為啟動函數。表示開始執行協程,即{}內部分。launch是最常用的啟動函數,另外還有asyncrunBlocking等。

  1. runBlocking:T 啟動一個新的協程并阻塞調用它的線程,直到里面的代碼執行完畢,返回值是泛型T,就是你協程體中最后一行是什么類型,最終返回的是什么類型T就是什么類型。
  2. launch:Job 啟動一個協程但不會阻塞調用線程,必須要在協程作用域(CoroutineScope)中才能調用,返回值是一個Job
  3. async:Deferred<T> 啟動一個協程但不會阻塞調用線程,必須要在協程作用域(CoroutineScope)中才能調用。以Deferred對象的形式返回協程任務。返回值泛型TrunBlocking類似都是協程體最后一行的類型。Deferred繼承自Job,我們可以把它看做一個帶有返回值的Job.

掛起函數

suspend是協程的關鍵字,表示這個一個掛起函數,每一個被suspend飾的方法只能在suspend方法或者在協程中調用。
一般耗時任務,或者功能性任務放在掛起函數中。

如網絡請求

	@FormUrlEncoded@POST("/login/user/xxx")suspend fun userLogin(@Field("cellphone") cellphone: String,@Field("captcha") captcha: String): AppResult<LoginResult>

協程調度器

協程調度器CoroutineDispatcher 是用來指定協程執行所在的線程或者調度器。
Kotlin 協程庫提供了幾個預定義的調度器,在封裝單例類Dispatchers中,如 Dispatcher.Main(用于UI線程)、Dispatcher.IO(用于I/O密集型任務)和 Dispatcher.Default(用于CPU密集型任務)。通過選擇合適的調度器,我們可以控制協程的執行環境,實現線程管理。

使用,在啟動函數中,傳入對應的調度器即可。如下面代碼:

lifecycleScope.launch(Dispatchers.IO) {//放在IO 線程中,處理耗時任務doCopyFile(src, dst)withContext(Dispatchers.Main) {// 編輯圖片}}

協程執行中間要切換線程怎么辦?我們可以再次調用launch啟動方法(不推薦),但是如果來回切換線程的次數過多,就會出現地獄式回調。我們也可以使用withContext.

withContext是一個頂級函數,使用withContext函數來改變協程的上下文,而仍然駐留在相同的協程中,同時withContext還攜帶有一個泛型T返回值。

如上述示例,如果我們想拷貝文件完成,在主線程做些使用,就可以這樣寫

lifecycleScope.launch(Dispatchers.IO) {//拷貝文件,放在IO 線程中,處理耗時任務doCopyFile(src, dst)withContext(Dispatchers.Main) {// 刷新美顏素材,放在主線程showBeautyView()}}

使用總結

至此,協程的三大件(CoroutineScopeDispatcherssuspend關鍵字)已經介紹完了。這三大件共同構成了Kotlin協程的核心機制,使得開發者能夠編寫高效、易于理解和維護的異步代碼。簡單的協程應用應該不成問題了。

協程的進階知識

協程上下文

CoroutineContext即協程上下文。CoroutineContext是一個非常核心的概念,它代表了協程執行的環境,包括協程的執行者(Dispatcher)、協程的父子關系、協程的元數據等。

它是一個包含了用戶定義的一些各種不同元素的Element對象集合。其中主要元素是Job協程調度器CoroutineDispatcher、還有包含協程異常CoroutineExceptionHandler攔截器ContinuationInterceptor協程名CoroutineName等。這些數據都是和協程密切相關的,每一個Element都一個唯一key。劃重點,后面的主要方法都是依據此特性。

CoroutineContext主要方法

plus方法
plus有個關鍵字operator表示這是一個運算符重載的方法,類似List.plus的運算符,這樣我們可以通過+操作符用于合并兩個CoroutineContext,創建一個新的CoroutineContext,這個新上下文包含了左右兩邊Context的所有元素。
這里的元素主要是指協程相關的屬性,如協程調度器(Dispatcher)、協程范圍(CoroutineScope)、協程名稱協程的父母關系等。當兩個Context中有重復的元素(如調度器),后者將會覆蓋前者,因為Context合并遵循 右優先 原則。

val baseContext = CoroutineContext(Dispatchers.Default)
//newContext將會使用Dispatchers.Main作為其調度器,
//因為它在合并過程中覆蓋了之前的Dispatchers.Default。
val newContext = baseContext + Dispatchers.Main

實際應用在實際開發中,+操作符經常用于在啟動協程時,通過擴展當前的上下文來指定額外的屬性,比如改變調度器、添加協程的名稱以便于調試等。如下示例:

launch(Dispatchers.IO + coroutineContext + CoroutineName("MyCoroutine")) {// 協程邏輯
}

通過這種方式,你可以靈活地組合和定制每個協程的執行環境,滿足特定的執行需求。

get方法

CoroutineContext中查詢指定類型的元素。如果找到了匹配的元素,它會返回該元素的實例;如果沒有找到,則返回null。這使得開發者能夠根據需要檢查協程上下文中是否存在特定的組件。

val context = Dispatchers.IO + CoroutineName("Coroutine1")
// 查詢CoroutineName元素
val nameElement = context.get<CoroutineName>()
minusKey方法

從當前的CoroutineContext中移除(排除)指定類型的元素。
調用minusKey,鍵(Key)參數跟上述+get一致,執行minusKey后,返回一個新的CoroutineContext,這個新的上下文是原上下文的一個子集,不包含被指定鍵所對應的元素。原CoroutineContext本身保持不變,因為它是不可變的。

fold方法

fold方法是一種用于將協程上下文中的元素聚合為單個值的高階函數。這個方法源自于函數式編程的概念,其基本思想是在一個累積值上應用一個二元操作,遍歷上下文中所有元素,最終得到一個結果值。
CoroutineContext的場景中,它允許你對上下文中的每個元素執行某種操作,并將這些操作的結果合并成一個最終結果。

//fold定義
//initial: 這是聚合操作的初始值,決定了最終結果的類型
//operation: 這是一個 lambda 函數,接收兩個參數:一個是當前的累積值(從initial開始),
//  另一個是正在處理的CoroutineContext.Element。這個函數定義了如何將當前元素與累積值結合,
//  返回一個新的累積值。
public inline fun <R> CoroutineContext.fold(initial: R, operation: (R, Element) -> R): R

fold示例: fold從初始值0開始,對于上下文中每個MyElement元素,它將當前累計值與該元素的value相加,最終得到所有MyElement的值之和。

class MyElement(val value: Int) : CoroutineContext.Elementval context = EmptyCoroutineContext +MyElement(1) +MyElement(2) +MyElement(3)// 使用fold方法計算所有MyElement的value之和
val sum = context.fold(0) { acc, element ->if (element is MyElement) acc + element.value else acc
}
println("Sum of values: $sum") // 輸出: Sum of values: 6

CoroutineContextfold方法提供了一種強大的方式來處理和聚合協程上下文中的信息,它允許開發者以聲明式的方式表達對上下文的復雜操作,提高了代碼的可讀性和靈活性。

協程作用域

協程作用域CoroutineScope為協程定義作用范圍,每個協程生成器launchasync等都是CoroutineScope的擴展函數,并繼承了它的coroutineContext自動傳播其所有Element和取消。
之前我們都是使用GlobalScope,或者android中的 LifeCycleScopeViewModelScope這些,我們能不能自己定義 協程作用域呢?

先看下CoroutineScope 的相關函數。

public interface CoroutineScope {public val coroutineContext: CoroutineContext
}
//CoroutineScope也重載了plus方法,通過+號來新增或者修改我們CoroutineContext協程上下文中的Element
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =ContextScope(coroutineContext + context)public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)public object GlobalScope : CoroutineScope {override val coroutineContext: CoroutineContextget() = EmptyCoroutineContext
}
//CoroutineScope的構造函數,參數中沒有job會新建一個job
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =ContextScope(if (context[Job] != null) context else context + Job())

看作用域的構造函數,參數只有一個CoroutineContext,也就是我們上面介紹的部分,自定義協程作用域,也就是是定義CoroutineContext.

自定義作用域,示例

val scope = CoroutineScope(Dispatchers.IO + CoroutineName("self define"))
scope.launch {Log.i("scope", "i am in a scope.${coroutineContext[CoroutineName]}")delay(2000)Log.i("scope", "i am in a scope, after do something")
}

協程異常的處理

執行一段代碼,可以會拋出異常,如果我們沒有try...catch,程序將停止執行。協程也是一樣,出現了異常,如果沒有處理,也會導致協程退出,甚至崩潰。協程的異常處理,使用CoroutineExceptionHandler捕獲,它也是CoroutineContext的一種。當然我們可以使用+拼接。

下面我們一步步深入,對協程的異常處理

  • 最簡單的不做任何異常處理,這個很好理解,和普通程序類型將導致崩潰。
  • 使用CoroutineExceptionHandler捕獲,默認情況下,它會將異常傳播到它的父級,父級會取消其余的子協程,同時取消自身的執行。可以這樣理解,對照普通代碼,相當于我們在父級作用域有一個try...catch當出現異常時,會走到異常處理代碼塊,其他邏輯都不執行了,父級作用域和子級作用域都不會執行。
  • 當出現異常時,如果我不想影響父級作用域,和兄弟作用域怎么辦呢,只需要將當前作用域Job替換為SupervisorJob即可。這時對比普通代碼,相當于我們在當前作用域加了一個try...catch,其他作用域邏輯正常執行。

默認情況下,當協程因出現異常失敗時,它會將異常傳播到它的父級,父級會取消其余的子協程,同時取消自身的執行。最后將異常在傳播給它的父級。當異常到達當前層次結構的根,在當前協程作用域啟動的所有協程都將被取消。

private fun testCoroutineSupervisorJob() {val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->Log.d("exceptionHandler", "-------${coroutineContext[CoroutineName]} $throwable")}val coroutineScope = CoroutineScope(CoroutineName("coroutineScope"))coroutineScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {val scope2 = launch(SupervisorJob()+ CoroutineName("scope2") + exceptionHandler) {Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}")throw  NullPointerException("空指針")}val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {scope2.join()Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}")delay(2000)Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}")}scope2.join()Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}")scope3.join()Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}")}}

上述代碼,輸出log

 1--------- CoroutineName(scope2)-------CoroutineName(scope2) java.lang.NullPointerException: 空指針2--------- CoroutineName(scope3)4--------- CoroutineName(scope1)3--------- CoroutineName(scope3)5--------- CoroutineName(scope1)

如果scope2為普通Job,走到異常處,代碼將不再執行。不會輸出2,4,3,5 大家可以試下。

SupervisorJob異常隔離性:SupervisorJob在協程作用域中,提供了異常隔離機制。如果作用域下的某個協程拋出了異常,它只會取消自己,而不會導致整個作用域或其它協程被取消。這對于構建健壯態系統特別關鍵,允許部分失敗而不影響全局。

協程還可以使用 supervisorScope函數,效果同SupervisorJob寫法不同。如下,log同上。

private fun testCoroutineSupervisorJob() {val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->Log.d("exceptionHandler", "-------${coroutineContext[CoroutineName]} $throwable")}val coroutineScope = CoroutineScope(CoroutineName("coroutineScope"))coroutineScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {supervisorScope {val scope2 = launch(CoroutineName("scope2") + exceptionHandler) {Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}")throw NullPointerException("空指針")}val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {scope2.join()Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}")delay(2000)Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}")}scope2.join()Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}")scope3.join()Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}")}}}

協程在android中的應用

Android開發中,我們常用到 lifecycleScope, viewModelScope
lifecycleScopeKotlin協程庫為Android應用特別設計的一個特性,它將協程的生命周期與ActivityFragment的生命周期綁定在一起,確保協程在相應的組件(如ActivityFragment)銷毀時能夠自動取消,從而避免內存泄漏和資源浪費。
ActivityFragment中使用lifecycleScope啟動的協程,無需手動管理協程的取消邏輯,因為當組件生命周期狀態變化時,lifecycleScope會自動處理協程的取消邏輯。
lifecycleScope能夠感知ActivityFragment的生命周期變化,當組件不再活動 ON_DESTROY時,協程會被取消。lifecycleScope提供了一些方法,可以在不同生命周期調用,如launchWhenCreated launchWhenStarted launchWhenResumed

viewModelScope為在ViewModel內部啟動的協程定義了一個明確的作用域。這意味著在ViewModel生命周期內啟動的協程將遵循ViewModel的生存周期,當ViewModel被清除時,所有相關的協程也將被取消,有助于資源管理。
通過viewModelScope,開發者無需手動處理協程的取消邏輯,ViewModel的生命周期會自動管理協程的生命周期,使得代碼更簡潔、易維護。

參考資料

史上最詳Android版kotlin協程入門進階實戰
Android Kotlin協程指南

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

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

相關文章

JDK、JRE、編譯指令和垃圾回收機制詳解

JDK 全稱 Java SE Development Kit (Java 開發工具包) JVM虛擬機&#xff1a;Java運行的地方 核心類庫&#xff1a;Java提前編好的東西 開發工具&#xff1a; javac,java,jdb,jhat javac:Java編譯器&#xff0c;用于將Java源代碼編譯成Java字節碼文件(.class)。 java: java…

[STM32-HAL庫]AS608-指紋識別模塊-STM32CUBEMX開發-HAL庫開發系列-主控STM32F103C8T6

目錄 一、前言 二、詳細步驟 1.光學指紋模塊 2.配置STM32CUBEMX 3.程序設計 3.1 輸出重定向 3.2 導入AS608庫 3.3 更改端口宏定義 3.4 添加中斷處理部分 3.5 初始化AS608 3.6 函數總覽 3.7 錄入指紋 3.8 驗證指紋 3.9 刪除指紋 3.10 清空指紋庫 三、總結及資源 一、前言 …

[力扣題解] 797. 所有可能的路徑

題目&#xff1a;797. 所有可能的路徑 思路 深度搜索 代碼 // 圖論哦!class Solution { private:vector<vector<int>> result;vector<int> path;// x : 當前節點void function(vector<vector<int>>& graph, int x){int i;// cout <&l…

解決鼠標滾動時element-ui日期選擇器錯位的問題

解決方案&#xff1a;監聽鼠標滾動事件&#xff0c;在鼠標滾動時隱藏element-ui日期選擇器下拉框 1、先在util文件夾下創建個hidePicker.js文件&#xff0c;代碼如下&#xff1a; let el nullconst fakeClickOutSide () > {const SELECTWRAP_BODY document.body // bod…

Day37 貪心算法part04

LC860檸檬水找零(未掌握) 未掌握分析&#xff1a;20的時候找零卡住&#xff0c;同時貪心思路就想了很久 當bill[i]20的時候&#xff0c;我們有兩種找零范式&#xff0c;找零10、5和找零三個5&#xff0c;優先找零10、5&#xff0c;因為三個5是可以替代10、5的情況的&#xff0…

Nebula街機模擬器 Mac移植版(400+游戲roms)漢化版

nebula星云模擬器是電腦上最熱門的街機游戲模擬器之一&#xff0c;玩家可以通過這個小巧的模擬器軟件進行多款經典街機游戲啟動和暢玩&#xff0c;本次移植的包含400多款游戲roms&#xff0c;經典的三國志、三國戰紀、拳皇、街霸、合金彈頭、1941都包含在內。 下載地址&#xf…

CompletableFuture的主要用途是什么?

CompletableFuture 的主要用途是為復雜的異步編程模型提供一種更簡單&#xff0c;更具可讀性的方式。它主要用于以下幾個方面&#xff1a; 非阻塞計算&#xff1a;CompletableFuture 為處理高延遲的計算任務提供了非阻塞的解決方案。你可以啟動一個計算任務&#xff0c;而不需要…

前端 CSS 經典:好看的標題動畫

前言&#xff1a;好看的標題動畫實現。 效果&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><…

YOLOv5 AssertionError: “XXX” acceptable suffix is [‘.pt‘]

使用終端訓練YOLOv5模型報錯&#xff0c;原命令為&#xff1a; “python train.py --img 640 --batch 1 --epochs 25 --data "C:\Users\GRT\PycharmProjects\yolov5-7.0\animal_training\dataset.yaml " --weights “C:\Users\GRT\PycharmProjects\yolov5-7.0\MyFunc…

組播協議簡介

一、組播協議介紹 組播協議是一種網絡通信協議&#xff0c;它允許一個發送者同時向多個接收者發送數據。以下是組播協議的一些特點&#xff1a; 高效性&#xff1a;組播協議可以有效地利用網絡帶寬&#xff0c;因為它只需要發送一份數據副本&#xff0c;就可以被多個接收者同…

藍橋樓賽第30期-Python-第三天賽題 從參數中提取信息題解

樓賽 第30期 Python 模塊大比拼 提取用戶輸入信息 介紹 正則表達式&#xff08;英文為 Regular Expression&#xff0c;常簡寫為regex、regexp 或 RE&#xff09;&#xff0c;也叫規則表達式、正規表達式&#xff0c;是計算機科學的一個概念。 所謂“正則”&#xff0c;可以…

docker swarm多主機之間的端口無法訪問,但能ping通 問題排查及解決

已排查&#xff1a;1.ufw status 防火墻已關閉 2.selinux已關閉 3.netstat -ntpl :::8088 未限制ip 問題&#xff1a;docker swarm多主機之間的端口無法訪問&#xff0c;但能ping通&#xff0c;同一主機下的端口也可以訪問。 原因&#xff1a;docker overlay網絡內部使用…

【Linux取經路】初識線程——線程控制

文章目錄 一、什么是線程&#xff1f;1.1 Linux 中線程該如何理解&#xff1f;1.2 如何理解把資源分配給線程&#xff1f;1.2.1 虛擬地址到物理地址的轉換 1.3 線程 VS 進程1.3.1 線程為什么比進程更輕量化&#xff1f;1.3.2 線程的優點1.3.3 線程缺點1.3.4 線程異常1.3.5 線程…

關于基礎的流量分析(1)

1.對于流量分析基本認識 1&#xff09;簡介&#xff1a;網絡流量分析是指捕捉網絡中流動的數據包&#xff0c;并通過查看包內部數據以及進行相關的協議、流量分析、統計等來發現網絡運行過程中出現的問題。 2&#xff09;在我們平時的考核和CTF比賽中&#xff0c;基本每次都有…

MySQL用戶管理操作

用戶權限管理操作 DCL語句 一.用戶管理操作 MySQL軟件內部完整的用戶格式&#xff1a; 用戶名客戶端地址 admin1.1.1.1這個用戶只能從1.1.1.1的客服端來連接服務器 admin1.1.1.2這個用戶只能從1.1.1.2的客服端來連接服務器 rootlocal host這個用戶只能從服務器本地進行連…

Prompt - 流行的10個框架

轉載自&#xff1a;https://juejin.cn/post/7287412759050289212 文章目錄 1、ICIO框架2、CRISPE框架3、BROKE框架4、CREATE框架5、TAG框架6、RTF框架7、ROSES框架8、APE框架9、RACE框架10、TRACE框架 測試用例 為了看到不同的Prompt框架效果&#xff0c;本文定義一個統一的測…

ACM實訓

【碎碎念】繼續搞習題學習&#xff0c;今天完成第四套的ABCD&#xff0c;為下一周擠出時間復習&#xff0c;加油 Digit Counting 問題 法希姆喜歡解決數學問題。但有時解決所有的數學問題對他來說是一個挑戰。所以有時候他會為了解決數學難題而生氣。他拿起一支粉筆&#xff…

Java面試八股之進程和線程的區別

Java進程和線程的區別 定義與作用&#xff1a; 進程&#xff1a;在操作系統中&#xff0c;進程是程序執行的一個實例&#xff0c;是資源分配的最小單位。每個進程都擁有獨立的內存空間&#xff0c;包括代碼段、數據段、堆空間和棧空間&#xff0c;以及操作系統分配的其他資源…

工廠模式(簡單工廠模式+工廠模式)

工廠模式的目的就是將對象的創建過程隱藏起來&#xff0c;從而達到很高的靈活性&#xff0c;工廠模式分為三類&#xff1a; 簡單工廠模式工廠方法模式抽象工廠模式 在沒有工廠模式的時候就是&#xff0c;客戶需要一輛馬車&#xff0c;需要客戶親自去創建一輛馬車&#xff0c;…

PDF之Blend Mode(混合模式)BM(對應OFD的BlendMode)

Blend Mode&#xff08;混合模式&#xff09;用于定義對象與背景或其他對象之間的顏色混合方式。PDF支持多種混合模式&#xff0c;常見的混合模式包括&#xff1a; Normal&#xff1a;正常混合模式&#xff0c;將對象顏色直接疊加在背景上。 Multiply&#xff1a;乘法混合模式…