認識協程
引用官方的一段話
協程通過將復雜性放入庫來簡化異步編程。程序的邏輯可以在協程中順序地表達,而底層庫會為我們解決其異步性。該庫可以將用戶代碼的相關部分包裝為回調、訂閱相關事件、在不同線程(甚至不同機器!)上調度執行,而代碼則保持如同順序執行一樣簡單。
協程是一種并發設計模式,您可以在Android平臺上使用它來簡化異步執行的代碼
簡單概括:以同步的方式去編寫異步執行的代碼。協程是依賴于線程,但是協程掛起時不需要阻塞線程,幾乎是無代價的。
協程的實現,會用到線程,但是使用協程不用類比線程,跟線程是不同的概念。
Android項目引入協程
- 在項目
根build.gradle
-buildscript
-dependencies
下引入kotlin
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- 在項目各
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
是最常用的啟動函數,另外還有async
,runBlocking
等。
runBlocking:T
啟動一個新的協程并阻塞調用它的線程,直到里面的代碼執行完畢,返回值是泛型T
,就是你協程體中最后一行是什么類型,最終返回的是什么類型T
就是什么類型。launch:Job
啟動一個協程但不會阻塞調用線程,必須要在協程作用域(CoroutineScope
)中才能調用,返回值是一個Job
。async:Deferred<T>
啟動一個協程但不會阻塞調用線程,必須要在協程作用域(CoroutineScope
)中才能調用。以Deferred
對象的形式返回協程任務。返回值泛型T
同runBlocking
類似都是協程體最后一行的類型。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()}}
使用總結
至此,協程的三大件(CoroutineScope
、Dispatchers
、suspend關鍵字
)已經介紹完了。這三大件共同構成了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
CoroutineContext
的fold
方法提供了一種強大的方式來處理和聚合協程上下文中的信息,它允許開發者以聲明式的方式表達對上下文的復雜操作,提高了代碼的可讀性和靈活性。
協程作用域
協程作用域CoroutineScope
為協程定義作用范圍,每個協程生成器launch
、async
等都是CoroutineScope
的擴展函數,并繼承了它的coroutineContext
自動傳播其所有Element
和取消。
之前我們都是使用GlobalScope
,或者android中的 LifeCycleScope
,ViewModelScope
這些,我們能不能自己定義 協程作用域呢?
先看下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
lifecycleScope
是Kotlin
協程庫為Android
應用特別設計的一個特性,它將協程的生命周期與Activity
或Fragment
的生命周期綁定在一起,確保協程在相應的組件(如Activity
或Fragment
)銷毀時能夠自動取消,從而避免內存泄漏和資源浪費。
在Activity
或Fragme
nt中使用lifecycleScope
啟動的協程,無需手動管理協程的取消邏輯,因為當組件生命周期狀態變化時,lifecycleScope
會自動處理協程的取消邏輯。
lifecycleScope
能夠感知Activity
或Fragment
的生命周期變化,當組件不再活動 ON_DESTROY
時,協程會被取消。lifecycleScope
提供了一些方法,可以在不同生命周期調用,如launchWhenCreated
launchWhenStarted
launchWhenResumed
viewModelScope
為在ViewModel
內部啟動的協程定義了一個明確的作用域。這意味著在ViewModel
生命周期內啟動的協程將遵循ViewModel
的生存周期,當ViewModel
被清除時,所有相關的協程也將被取消,有助于資源管理。
通過viewModelScope
,開發者無需手動處理協程的取消邏輯,ViewModel
的生命周期會自動管理協程的生命周期,使得代碼更簡潔、易維護。
參考資料
史上最詳Android版kotlin協程入門進階實戰
Android Kotlin協程指南