前言
公司開啟新項目了,想著準備亮一手 Kotlin 協程應用到項目中去,之前有對 Kotlin 協程的知識進行一定量的學習,以為自己理解協程了,結果……實在拿不出手!
為了更好的加深記憶和理解,更全面系統深入地學習 Kotlin 協程的知識,協程將分為三部分來講解,本文是第一篇
一、概述
協程的概念在1958年就開始出現(比線程還早), 目前很多語言開始原生支, Java 沒有原生協程但是大型公司都自己或者使用第三方庫來支持協程編程, 但是Kotlin原生支持協程。
Android 中的每個應用都會運行一個主線程,它主要是用來處理 UI,如果主線程上需要處理的任務太多,應用就感覺被卡主一樣影響用戶體驗,得讓那些耗時的任務不阻塞主線程的運行。要做到處理網絡請求不會阻塞主線程,一個常用的做法就是使用回調,另一種是使用協程。
協程概念
很多人都會問協程是什么?這里引用官方的解釋:
1.協程通過將復雜性放入庫來簡化異步編程。程序的邏輯可以在協程中順序地表達,而底層庫會為我們解決其異步性。該庫可以將用戶代碼的相關部分包裝為回調、訂閱相關事件、在不同線程(甚至不同機器)上調度執行,而代碼則保持如同順序執行一樣簡單。
2.協程是一種并發設計模式。
協程就像輕量級的線程,為什么是輕量的?因為協程是依賴于線程,一個線程中可以創建N個協程, 很重要的一點就是協程掛起時不會阻塞線程 ,幾乎是無代價的。而且它 基于線程池API ,所以在處理并發任務這件事上它真的游刃有余。
協程只是一種概念,它提供了一種避免阻塞線程并用更簡單、更可控的操作替代線程阻塞的方法: 協程掛起和恢復 。 本質上Kotlin協程就是作為在Kotlin語言上進行異步編程的解決方案,處理異步代碼的方法 。
有可能有的同學問了,既然它基于線程池,那我直接使用線程池或者使用 Android 中其他的異步任務解決方案,比如 Handler、AsyncTask、RxJava等,不更好嗎?
協程可以 使用阻塞的方式寫出非阻塞式的代碼 ,解決并發中常見的回調地獄。消除了并發任務之間的協作的難度,協程可以讓我們輕松地寫出復雜的并發代碼。一些本來不可能實現的并發任務變的可能,甚至簡單,這些才是協程的優勢所在。
作用
- 1.協程可以讓異步代碼同步化 ;
- 2.協程可以降低異步程序的設計復雜度 。
特點
- 輕量 :您可以在單個線程上運行多個協程,因為協程支持掛起,不會使正在運行協程的線程阻塞。掛起比阻塞節省內存,且支持多個并行操作。
- 內存泄漏更少 :使用結構化并發機制在一個作用域內執行多項操作。
- 內置取消支持 :取消操作會自動在運行中的整個協程層次結構內傳播。
- Jetpack 集成 :許多 Jetpack 庫都包含提供全面協程支持的擴展。某些庫還提供自己的協程作用域,可供您用于結構化并發。
Kotlin Coroutine 生態
kotlin的協程實現分為了兩個層次:
- 基礎設施層 :標準庫的協程API,主要對協程提供了概念和語義上最基本的支持;
- 業務框架層 kotlin.coroutines :協程的上層框架支持,基于標準庫實現的封裝,也是我們日常開發使用的協程擴展庫。
依賴庫
在 project
的 gradle
添加 Kotlin
編譯插件:
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
}
要使用協程,還需要在app的 build.gradle
文件中添加依賴:
dependencies {
//協程標準庫
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
//協程核心庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
//協程Android支持庫,提供安卓UI調度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}
這里我們主要使用協程擴展庫, kotlin協程標準庫太過于簡陋不適用于開發者使用。
二、原理
協程的概念最核心的點就是函數或者一段程序能夠被掛起,稍后再在掛起的位置恢復 。協程通過主動讓出運行權來實現協作,程序自己處理掛起和恢復來實現程序執行流程的協作調度。因此它本質上就是在討論程序控制流程的機制。
使用場景
kotlin協程基于Thread相關API的封裝,讓我們不用過多關心線程也可以方便地寫出并發操作,這就是Kotlin的協程。協程的好處本質上和其他線程api一樣, 方便 。
在 Android 平臺上,協程有兩個主要使用場景:
- 1、線程切換,保證線程安全。
- 2、處理耗時任務(比如網絡請求、解析
JSON
數據、從數據庫中進行讀寫操作等)。
Kotlin協程的原理
我們使用 Retrofit
發起了一個異步請求,從服務端查詢用戶的信息,通過 CallBack
返回 response
:
val call: Call
很明顯我們需要處理很多的回調分支,如果業務多則更容易陷入「回調地獄」繁瑣凌亂的代碼中。
使用協程,同樣可以像 Rx 那樣有效地消除回調地獄,不過無論是設計理念,還是代碼風格,兩者是有很大區別的,協程在寫法上和普通的順序代碼類似,同步的方式去編寫異步執行的代碼。使用協程改造后代碼如下:
GlobalScope.launch(Dispatchers.Main) {//開始協程:主線程
val result = userApi.getUserSuspend("suming")//網絡請求(IO 線程)
tv_name.text = result?.name //更新 UI(主線程)
}
這就是kotlin最有名的【非阻塞式掛起】,使用同步的方式完成異步任務,而且很簡潔,這是Kotlin協程的魅力所在。之所有可以用看起來同步的方式寫異步代碼,關鍵在于請求函數getUserSuspend()
是一個 掛起函數 ,被suspend
關鍵字修飾,下面會介紹。
在上面的協程的原理圖解中,耗時阻塞的操作并沒有減少,只是交給了其他線程。userApi.getUserSuspend("suming")
真正執行的時候會切換到IO線程中執行,獲取結果后最后恢復到主線程上,然后繼續執行剩下的流程。
將業務流程原理拆分得更細致一點,在主線程中創建協程A
中執行整個業務流程,如果遇到異步調用任務則協程A
被掛起,切換到IO線程中創建子協程B
,獲取結果后再恢復到主線程的協程A
上,然后繼續執行剩下的流程。
協程Coroutine雖然不能脫離線程而運行,但可以在不同的線程之間切換,而且一個線程上可以一個或多個協程。下圖動態顯示了進程 - 線程 - 協程微妙關系。
此動圖來源
三、基礎
GlobalScope.launch(Dispatchers.Main) {//開始協程:主線程
val result = userApi.getUserSuspend("suming")//網絡請求(IO 線程)
tv_name.text = result?.name //更新 UI(主線程)
}
上面就是啟動協程的代碼,啟動協程的代碼可以分為三部分:GlobalScope
、launch
、Dispatchers
,它們分別對應:協程的作用域、構建器和調度器。
1.協程的構建
上面的GlobalScope.launch()
屬于協程構建器Coroutine builders
,Kotlin 中還有其他幾種 Builders, 負責創建協程 :
runBlocking:T
:頂層函數,創建一個新的協程同時阻塞當前線程,直到其內部所有邏輯以及子協程所有邏輯全部執行完成,返回值是泛型T
,一般在項目中不會使用,主要是為main函數和測試設計的。launch
:?創建一個新的協程,不會阻塞當前線程,必須在協程作用域中才可以調用。它返回的是一個該協程任務的引用,即Job
對象。這是最常用的用于啟動協程的方式。async
:?創建一個新的協程,不會阻塞當前線程,必須在協程作用域中才可以調用。并返回Deffer
對象,可通過調用Deffer.await()
方法等待該子協程執行完成并獲取結果。常用于并發執行-同步等待和獲取返回值的情況。
runBlocking
fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
- context:??協程的上下文,表示協程的運行環境,包括協程調度器、代表協程本身的Job、協程名稱、協程ID等,默認值是當前線程上的事件循環。(這里的
context
和Android的context
不同,后面會講解到) - block:???協程執行體,是一個用suspend關鍵字修飾的一個無參,無返回值的函數類型。是一個帶接收者的函數字面量,接收者是 CoroutineScope ,因此執行體包含了一個隱式的
CoroutineScope
,所以在runBlocking
內部可以來直接啟動協程。 - T:?????返回值是泛型
T
,協程體block
中最后一行返回的是什么類型T
就是什么類型。
它是一個頂層函數,不是GlobalScope
的 API,可以在任意地方獨立使用。它能創建一個新的協程同時阻塞當前線程,直到其內部所有邏輯以及子協程所有邏輯全部執行完成,它的目的是將常規的阻塞代碼與以掛起suspend
風格編寫的庫連接起來,常用于main
函數和測試中。一般我們在項目中是不會使用的。
fun runBloTest() {
print("start")
//context上下文使用默認值,阻塞當前線程,直到代碼塊中的邏輯完成
runBlocking {
//這里是協程體
delay(1000)//掛起函數,延遲1000毫秒
print("runBlocking")
}
print("end")
}
打印數據如下:
runBlocking.gif
只有在runBlocking
協程體邏輯全部運行結束后,聲明在runBlocking
之后的代碼才能執行,即runBlocking
會阻塞其所在線程。
注意:runBlocking
雖然會阻塞當前線程的,但其內部運行的協程又是非阻塞的。
launch
launch
是最常用的用于啟動協程的方式,用于在不阻塞當前線程的情況下啟動一個協程,并返回對該協程任務的引用,即Job
對象。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
- context:?協程的上下文,表示協程的運行環境,包括協程調度器、代表協程本身的Job、協程名稱、協程ID等,默認值是當前線程上的事件循環。
- start: ?協程啟動模式,這些啟動模式的設計主要是為了應對某些特殊的場景。業務開發實踐中通常使用DEFAULT和LAZY這兩個啟動模式就夠了。
- block:??協程代碼,它將在提供的范圍的上下文中被調用。它是一個用
suspend
(掛起函數)關鍵字修飾的一個無參,無返回值的函數類型。接收者是CoroutineScope
的函數字面量。 - Job:???協程構建函數的返回值,可以把
Job
看成協程對象本身,封裝了協程中需要執行的代碼邏輯,是協程的唯一標識,Job可以取消,并且負責管理協程的生命周期。
協程需要運行在協程上下文環境中 (即協程作用域,下面會講解到),在非協程環境中launch
有兩種方式創建協程:
GlobalScope.launch()
在應用范圍內啟動一個新協程,不會阻塞調用線程,協程的生命周期與應用程序一致。表示一個不綁定任何Job
的全局作用域,用于啟動頂層協程,這些協程在整個應用程序生命周期中運行,不會提前取消(不存在Job
)。
fun launchTest() {
print("start")
//創建一個全局作用域協程,不會阻塞當前線程,生命周期與應用程序一致
GlobalScope.launch {
//在這1000毫秒內該協程所處的線程不會阻塞
//協程將線程的執行權交出去,該線程繼續干它要干的事情,到時間后會恢復至此繼續向下執行
delay(1000)//1秒無阻塞延遲(默認單位為毫秒)
print("GlobalScope.launch")
}
print("end")//主線程繼續,而協程被延遲
}
GlobalScope.launch()
協程將線程的執行權交出去,該線程繼續干它要干的事情,主線程繼續,而協程被延遲,到時間后會恢復至此繼續向下執行。
打印數據如下:
launch1.gif
由于這樣啟動的協程存在組件已被銷毀但協程還存在的情況,極限情況下可能導致資源耗盡,尤其是在 Android 客戶端這種需要頻繁創建銷毀組件的場景,因此不推薦這種用法。
注意:這里說的是GlobalScope
沒有Job
, 但是啟動的launch
是有Job
的。 GlobalScope
本身就是一個作用域, launch
屬于其子作用域。
CoroutineScope.launch()
啟動一個新的協程而不阻塞當前線程,并返回對協程的引用作為一個Job
。通過CoroutineContext
至少一個協程上下文參數創建一個 CoroutineScope
對象。協程上下文控制協程生命周期和線程調度,使得協程和該組件生命周期綁定,組件銷毀時,協程一并銷毀,從而實現安全可靠地協程調用。這是在應用中最推薦使用的協程使用方式。
fun launchTest2() {
print("start")
//開啟一個IO模式的協程,通過協程上下文創建一個CoroutineScope對象,需要一個類型為CoroutineContext的參數
val job = CoroutineScope(Dispatchers.IO).launch {
delay(1000)//1秒無阻塞延遲(默認單位為毫秒)
print("CoroutineScope.launch")
}
print("end")//主線程繼續,而協程被延遲
}
-
Android
+關注
關注
12文章
3945瀏覽量
127947 -
JAVA
+關注
關注
19文章
2975瀏覽量
105156 -
ui
+關注
關注
0文章
204瀏覽量
21419 -
kotlin
+關注
關注
0文章
60瀏覽量
4211
發布評論請先 登錄
相關推薦
評論