掌握Kotlin Coroutine之 基礎概念
這是 掌握Kotlin Coroutine 系列文章第一節內容。之所以取名為 掌握Kotlin Coroutine 而不是 精通 是由於 Coroutine 是一個新的特性,也剛剛正式釋出沒多久,我也並沒有經常使用過,所以有些概念或者背後的原理我自己也不是很瞭解,或者理解的不是很正確。本系列文章只是總結了自己學習過程中對 Coroutine 的理解,整理成文方便自己梳理一下對 Coroutine 的瞭解,所以文中不可避免會出現一些理解不準確的地方,歡迎大家在公眾號(ID: yunzaiqianfeng )留言交流。
由於 Kotlin 語言創立之初就考慮了要和 Java 語言可以互相呼叫(現在也支援 JavaScript 了),所以 Kotlin 語言中的非同步 API Coroutine 的設計就比較簡單並且可以比較簡便的配合其他語言中的非同步 API 一起使用。 本文主要介紹 Coroutine 的一些基本概念以及在 Android 應用中的使用。
在專案中引用 Coroutine 庫
Kotlin Coroutine 有很多模組,不同的模組應用在不同的環境下,而要在 Android 應用中使用 Coroutine 則需要在專案中新增兩個基礎的依賴模組:
<br />dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1' }
小提示:程式碼可以左右拖動檢視,下同!
kotlinx-coroutines-core 為 Coroutine 的核心 API, 而kotlinx-coroutines-android 為安卓平臺的一些提供了一些支援,特別是提供了 Dispatchers.Main 這個 UI Dispatcher。
如果專案使用了 ProGuard 而不是 R8 來混淆程式碼的話, 還需要新增下面的 ProGuard 配置:
# ServiceLoader support -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} -keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {} -keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {} # Most of volatile fields are updated with AFU and should not be mangled -keepclassmembernames class kotlinx.** { volatile <fields>; }
需要注意的是, Coroutine 在 18年10月底正式釋出 1.0.0 版本,該版本需要和 Kotlin 1.3 版本一起使用。而最新的 Coroutine 版本為 1.1.1, 對應的 Kotlin 版本為 1.3.20。 所以需要記得在專案的 Gradle 配置檔案中設定 Kotlin 語言的版本號:ext.kotlin_version = ‘1.3.20’。
初窺 Coroutine
在介紹 Coroutine 概念之前先來看一個簡單的示例,通過程式碼體驗一下 Coroutine 的基本用法以及和 Java(Android) 非同步處理的不同之處。
通過 Android Studio 建立一個新的專案,選擇 Kotlin 語言和 AndroidX 庫,然後在專案中按照上面的方式來新增 Coroutine 依賴庫即可動手開始體驗 Coroutine 了。
@ObsoleteCoroutinesApi val BG = newSingleThreadContext("Background") @ObsoleteCoroutinesApi class MainActivity : AppCompatActivity() { lateinit var job: Job; override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) GlobalScope.launch { Log.d(TAG, "no dispatcher coroutine ${Thread.currentThread().name}") } Log.d(TAG, "onCreate: ${Thread.currentThread().name}") job = GlobalScope.launch(Dispatchers.Main) { Log.d(TAG, "in coroutine a: ${Thread.currentThread().name}") delay(10000) background() delay(10000) Log.d(TAG, "in coroutine b: ${Thread.currentThread().name}") textView.text = Date().toLocaleString() } textView.text = "Main" Log.d(TAG, "onCreate after launch ${Thread.currentThread().name}") fab.setOnClickListener { view -> job.cancel() Snackbar.make(view, "cancel job ${job.isCancelled}", Snackbar.LENGTH_LONG).show() } } suspend fun background() { withContext(BG) { Log.d(TAG, "background() called ${Thread.currentThread().name}") } } }
上面是在 Activity 的 onCreate 函式中通過 launch
這個函式來建立了兩個 Coroutine,其中第二個 Coroutine 指定了 Dispatchers.Main
這個 Context ,所以在第二個 Coroutine 中可以直接訪問 textView 並設定其文字內容為當前日期。
執行上面的程式碼,在開啟的介面上會先顯示 “Main” 文字,然後20秒後顯示當前日期和時間。 檢視 LogCat 顯示如下的 Log 資訊:
03-02 07:40:13.672 D/MainActivity: onCreate: main 03-02 07:40:13.672 D/MainActivity: onCreate after launch main 03-02 07:40:13.673 D/MainActivity: no dispatcher coroutine DefaultDispatcher-worker-1 03-02 07:40:13.684 D/MainActivity: in coroutine a: main 03-02 07:40:23.686 D/MainActivity: background() called Background 03-02 07:40:33.688 D/MainActivity: in coroutine b: main
第一、二行 Log 顯示在 main
執行緒執行;第三行 Log 顯示的是第一個 Coroutine 在 DefaultDispatcher-worker-1
執行緒中執行,這個執行緒為 Coroutine 預設的執行緒;第三、五行 Log 顯示是在 main
執行緒執行,這是因為這個 Coroutine 使用了 Dispatchers.Main
這個 context, 而第四行 log 顯示 background 這個函式是在 Background
這個執行緒執行, Background
執行緒是通過 val BG = newSingleThreadContext("Background")
來建立的,在定義 background()
函式的時候,使用 withContext(BG)
來限定裡面的程式碼執行的 context 為 BG
。
另外上面的程式碼使用了 delay
函式來延遲了 Coroutine 的執行,注意看最後三行 Log 的時間,分別為 40:13、40:23、40:33. 說明 launch
裡面的程式碼被阻塞了 10秒,並且這個 launch
裡面的程式碼是在 main 執行緒執行的,為啥沒有導致 ANR 呢? 我們先記住這些問題,下面來介紹 Coroutine 的一些概念。
Coroutine 基本概念
先來看一組 Coroutine 中的一些術語。
coroutine: coroutine 是一個 suspendable computation
例項。 suspendable computation
翻譯過來就是可以被暫停的計算邏輯,類似於 Java 中執行緒的概念,需要執行一個程式碼塊並且具有和執行緒類似的生命週期(建立狀態、開始狀態),和執行緒不同的是 Coroutine 並沒有和具體的某一個執行緒繫結到一起,Coroutine 要比執行緒輕量級多了,消耗的資源也少多了。多個 Coroutine 可以執行在同一個執行緒中。Coroutine 可以在一個執行緒上被暫停執行(Suspend),然後在另外一個執行緒恢復執行(Resume)。並且還可以具有 Future
或者 Promise
的特性 — 在執行完成的時候返回一個結果。
coroutine builder: coroutine builder
是一個用來建立一個 coroutine 例項的函式。Kotlin 定義了一些基礎的建構 Coroutine 的函式,比如 launch{}
、 future{}
、 sequence{}
等。使用這些基礎的建構函式可以構造其他 Coroutine 例項。
suspending function: suspending function
是一個帶有 suspend
修飾符的函式,比如上面例子中的 background()
函式。 這種函式可以在不阻塞當前執行緒的情況下呼叫其他耗時較長的 suspending function
。 suspending function
只能在 Coroutine 裡面或者其他 suspending function
中被呼叫, 在普通的 Kotlin 程式碼中無法呼叫。標準庫中提供了一些基礎的 suspending function
,比如上面用到的 delay()
函式。在 Android Studio 中 suspending function
旁邊會有一個特殊的標記符號,告訴開發者這是一個 suspending function
。如下圖:
suspending lambda: 普通的函式有 lambda
表示式形式,所以 suspending function
也有 lambda
表示式形式,和普通的表示式相比, suspending lambda
只是多了一個 suspend
識別符號。比如系統 launch
函式的最後一個引數就是 suspending lambda
:
`` public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { //... } ```
注意看上面 launch 函式第三個引數 block 的型別為 suspend CoroutineScope.() -> Unit,這個型別被稱之為 suspending function
型別。
suspending function type: suspending function 型別
就是一個標識 suspending 函式和 lambda 的函式型別,如上所示,使用 suspend 識別符號。再比如 suspend () -> Int
是一個 suspending
函式型別 這個函式沒有引數返回 Int 型別的值。 而 suspend fun foo(): Int
函式就是一個符合這個型別定義的函式。
由於在 Kotlin 中函式也是一個型別定義,所以有普通的函式型別和 suspending 函式型別。
suspension point: suspension point
是 Coroutine 執行過程中遇到的可以被暫停執行的地方。通常而言, suspension point
是呼叫 suspending function
的地方,但是隻有當 suspending function
呼叫標準庫提供的用來暫停 Coroutine 執行的函式時,Coroutine 才會真正的停止執行。 比如 delay()
就是標準庫提供的一個暫停當前 Coroutine 一段時間的函式。
continuation: continuation
代表 Coroutine 在 suspension point
被暫停時的狀態。
下面根據上面的示例程式碼來和上面的概念做一下對應:
job = GlobalScope.launch(Dispatchers.Main) { Log.d(TAG, "in coroutine a: ${Thread.currentThread().name}") delay(10000) background() delay(10000) Log.d(TAG, "in coroutine b: ${Thread.currentThread().name}") textView.text = Date().toLocaleString() }
上面的程式碼中, GlobalScope.launch
為 coroutine builder
,該程式碼建立了一個 Coroutine 例項 job
,通過 job
可以檢視當前 Coroutine 執行的狀態並且可以控制 Coroutine,比如可以通過 job.cancel()
來取消該 Coroutine 的執行,和取消執行緒的執行概念類似。 launch
函式有三個引數,其中第三個引數為 suspending function type
函式型別,所以上面示例中 launch 的第三個引數的程式碼塊為 suspending lambda
, delay()
函式為 Coroutine 標準庫中提供的一個可以用來暫停當前執行緒執行的函式,所以當執行到 delay()
的時候,這個 Coroutine 就被暫停了,這個時候就是 suspension point
,當 Coroutine 從 delay()
返回的時候, 繼續恢復執行 background()
這個自定義的 suspending function
,在 background()
這個函式裡面使用 withContext()
函式來建立一個新的 子 Coroutine ,並且用其提供的 context 來執行所定義的程式碼塊( suspending lambda
)。
所以綜合來看,上面的程式碼在 UI 執行緒建立了一個在 Dispatchers.Main
執行緒(也是UI執行緒)執行的 Coroutine,Coroutine 並不會阻塞當前執行緒,所以當 Coroutine 裡面的程式碼在被暫停執行的時候, UI 執行緒也不會被阻塞,而當 Coroutine 恢復執行的時候, 程式碼繼續執行。 看起來是不是和 Java 裡面的執行緒、Android 裡面的 AsyncTask 等概念很類似呢? 只不過 Coroutine 的程式碼看起來更加簡潔(當然了,一部分歸功於 lambda 語法),並且當從其他執行緒獲取返回值的時候可以避免回撥函式巢狀的問題。
本節使用一個示例程式碼來介紹了 Coroutine 的各種基本概念和基本用法,後續章節將繼續深入介紹 Coroutine 的方方面面。