Android進階之Kotin協程原理和啟動方式詳細講解(優雅使用協程)
前言
kotlin的協程在初學者看來是一個很神奇的東西,居然能做到用同步的代碼塊實現異步的調用,其實深入了解你會發現kotlin協程本質上是通過函數式編程的風格對Java線程池的一種封裝,這樣會帶來很多好處,首先是函數式+響應式編程風格避免了回調地獄,這也可以說是實現promise,future等語言(比如js)的進一步演進。其次是能夠避免開發者的失誤導致的線程切換過多的性能損失。
那么我們就來看看協程
一、協程(Coroutines)是什么
1、協程是輕量級線程
- 協程是一種并發設計模式,您可以在 Android 平臺上使用它來簡化異步執行的代碼。
- 協程就是方法調用封裝成類線程的API。方法調用當然比線程切換輕量;而封裝成類線程的API后,它形似線程(可手動啟動、有各種運行狀態、能夠協作工作、能夠并發執行)。因此從這個角度說,它是輕量級線程沒錯;
- 當然,協程絕不僅僅是方法調用,因為方法調用不能在一個方法執行到一半時掛起,之后又在原點恢復。這一點可以使用EventLoop之類的方式實現。想象一下在庫級別將回調風格或Promise/Future風格的異步代碼封裝成同步風格,封裝的結果就非常接近協程;
2、線程運行在內核態,協程運行在用戶態
主要明白什么叫用戶態,我們寫的幾乎所有代碼,都執行在用戶態,協程對于操作系統來說僅僅是第三方提供的庫而已,當然運行在用戶態。而線程是操作系統級別的東西,運行在內核態。
3、協程是一個線程框架
Kotlin的協程庫可以指定協程運行的線程池,我們只需要操作協程,必要的線程切換操作交給庫,從這個角度來說,協程就是一個線程框架
4、協程實現
協程,顧名思義,就是相互協作的子程序,多個子程序之間通過一定的機制相互關聯、協作地完成某項任務。比如一個協程在執行上可以被分為多個子程序,每個子程序執行完成后主動掛起,等待合適的時機再恢復;一個協程被掛起時,線程可以執行其它子程序,從而達到線程高利用率的多任務處理目的——協程在一個線程上執行多個任務,而傳統線程只能執行一個任務,從多任務執行的角度,協程自然比線程輕量;
5、協程解決的問題
同步的方式寫異步代碼。如果不使用協程,我們目前能夠使用的API形式主要有三種:純回調風格(如AIO)、RxJava、Promise/Future風格,他們普遍存在回調地獄問題,解回調地獄只能通過行數換層數,且對于不熟悉異步風格的程序員來說,能夠看懂較為復雜的異步代碼就比較費勁。
6、協程優點
- 輕量:您可以在單個線程上運行多個協程,因為協程支持掛起,不會使正在運行協程的線程阻塞。掛起比阻塞節省內存,且支持多個并行操作。
- 內存泄漏更少:使用結構化并發機制在一個作用域內執行多項操作。
- 內置取消支持:取消操作會自動在運行中的整個協程層次結構內傳播。
- Jetpack 集成:許多 Jetpack 庫都包含提供全面協程支持的擴展。某些庫還提供自己的協程作用域,可供您用于結構化并發;
二、協程使用
- 依賴
- dependencies {
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
- }
協程需要運行在協程上下文環境,在非協程環境中憑空啟動協程,有三種方式
1、runBlocking{}
啟動一個新協程,并阻塞當前線程,直到其內部所有邏輯及子協程邏輯全部執行完成。
該方法的設計目的是讓suspend風格編寫的庫能夠在常規阻塞代碼中使用,常在main方法和測試中使用。
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- Log.e(TAG, "主線程id:${mainLooper.thread.id}")
- test()
- Log.e(TAG, "協程執行結束")
- }
- private fun test() = runBlocking {
- repeat(8) {
- Log.e(TAG, "協程執行$it 線程id:${Thread.currentThread().id}")
- delay(1000)
- }
- }

runBlocking啟動的協程任務會阻斷當前線程,直到該協程執行結束。當協程執行結束之后,頁面才會被顯示出來。
2、GlobalScope.launch{}
在應用范圍內啟動一個新協程,協程的生命周期與應用程序一致。這樣啟動的協程并不能使線程保活,就像守護線程。
由于這樣啟動的協程存在啟動協程的組件已被銷毀但協程還存在的情況,極限情況下可能導致資源耗盡,因此并不推薦這樣啟動,尤其是在客戶端這種需要頻繁創建銷毀組件的場景。
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- Log.e(TAG, "主線程id:${mainLooper.thread.id}")
- val job = GlobalScope.launch {
- delay(6000)
- Log.e(TAG, "協程執行結束 -- 線程id:${Thread.currentThread().id}")
- }
- Log.e(TAG, "主線程執行結束")
- }
- //Job中的方法
- job.isActive
- job.isCancelled
- job.isCompleted
- job.cancel()
- jon.join()
從執行結果看出,launch不會阻斷主線程。
下面我們來總結launch
我們看一下launch方法的定義:
- public fun CoroutineScope.launch(
- context: CoroutineContext = EmptyCoroutineContext,
- start: CoroutineStart = CoroutineStart.DEFAULT,
- block: suspend CoroutineScope.() -> Unit
- ): Job {
- val newContext = newCoroutineContext(context)
- val coroutine = if (start.isLazy)
- LazyStandaloneCoroutine(newContext, block) else
- StandaloneCoroutine(newContext, active = true)
- coroutine.start(start, coroutine, block)
- return coroutine
- }
從方法定義中可以看出,launch() 是CoroutineScope的一個擴展函數,CoroutineScope簡單來說就是協程的作用范圍。launch方法有三個參數:1.協程下上文;2.協程啟動模式;3.協程體:block是一個帶接收者的函數字面量,接收者是CoroutineScope
①.協程下上文
- 上下文可以有很多作用,包括攜帶參數,攔截協程執行等等,多數情況下我們不需要自己去實現上下文,只需要使用現成的就好。上下文有一個重要的作用就是線程切換,Kotlin協程使用調度器來確定哪些線程用于協程執行,Kotlin提供了調度器給我們使用:
- Dispatchers.Main:使用這個調度器在 Android 主線程上運行一個協程。可以用來更新UI 。在UI線程中執行
- Dispatchers.IO:這個調度器被優化在主線程之外執行磁盤或網絡 I/O。在線程池中執行
- Dispatchers.Default:這個調度器經過優化,可以在主線程之外執行 cpu 密集型的工作。例如對列表進行排序和解析 JSON。在線程池中執行。
- Dispatchers.Unconfined:在調用的線程直接執行。
- 調度器實現了CoroutineContext接口。
②.啟動模式
在Kotlin協程當中,啟動模式定義在一個枚舉類中:
- public enum class CoroutineStart {
- DEFAULT,
- LAZY,
- @ExperimentalCoroutinesApi
- ATOMIC,
- @ExperimentalCoroutinesApi
- UNDISPATCHED;
- }
一共定義了4種啟動模式,下表是含義介紹:
- DEFAULT:默認的模式,立即執行協程體
- LAZY:只有在需要的情況下運行
- ATOMIC:立即執行協程體,但在開始運行之前無法取消
- UNDISPATCHED:立即在當前線程執行協程體,直到第一個 suspend 調用
③.協程體
協程體是一個用suspend關鍵字修飾的一個無參,無返回值的函數類型。被suspend修飾的函數稱為掛起函數,與之對應的是關鍵字resume(恢復),注意:掛起函數只能在協程中和其他掛起函數中調用,不能在其他地方使用。
suspend函數會將整個協程掛起,而不僅僅是這個suspend函數,也就是說一個協程中有多個掛起函數時,它們是順序執行的。看下面的代碼示例:
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- GlobalScope.launch {
- val token = getToken()
- val userInfo = getUserInfo(token)
- setUserInfo(userInfo)
- }
- repeat(8){
- Log.e(TAG,"主線程執行$it")
- }
- }
- private fun setUserInfo(userInfo: String) {
- Log.e(TAG, userInfo)
- }
- private suspend fun getToken(): String {
- delay(2000)
- return "token"
- }
- private suspend fun getUserInfo(token: String): String {
- delay(2000)
- return "$token - userInfo"
- }
getToken方法將協程掛起,協程中其后面的代碼永遠不會執行,只有等到getToken掛起結束恢復后才會執行。同時協程掛起后不會阻塞其他線程的執行。
3.async/await:Deferred
async跟launch的用法基本一樣,區別在于:async的返回值是Deferred,將最后一個封裝成了該對象。async可以支持并發,此時一般都跟await一起使用,看下面的例子。
async和await是兩個函數,這兩個函數在我們使用過程中一般都是成對出現的;
async用于啟動一個異步的協程任務,await用于去得到協程任務結束時返回的結果,結果是通過一個Deferred對象返回的
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- GlobalScope.launch {
- val result1 = GlobalScope.async {
- getResult1()
- }
- val result2 = GlobalScope.async {
- getResult2()
- }
- val result = result1.await() + result2.await()
- Log.e(TAG,"result = $result")
- }
- }
- private suspend fun getResult1(): Int {
- delay(3000)
- return 1
- }
- private suspend fun getResult2(): Int {
- delay(4000)
- return 2
- }
async是不阻塞線程的,也就是說getResult1和getResult2是同時進行的,所以獲取到result的時間是4s,而不是7s。
三、協程異常
1、因協程取消,協程內部suspend方法拋出的CancellationException
2、常規異常,這類異常,有兩種異常傳播機制
- launch:將異常自動向父協程拋出,將會導致父協程退出
- async: 將異常暴露給用戶(通過捕獲deffer.await()拋出的異常)
例子講解
- fun main() = runBlocking {
- val job = GlobalScope.launch { // root coroutine with launch
- println("Throwing exception from launch")
- throw IndexOutOfBoundsException() // 我們將在控制臺打印 Thread.defaultUncaughtExceptionHandler
- }
- job.join()
- println("Joined failed job")
- val deferred = GlobalScope.async { // root coroutine with async
- println("Throwing exception from async")
- throw ArithmeticException() // 沒有打印任何東西,依賴用戶去調用等待
- }
- try {
- deferred.await()
- println("Unreached")
- } catch (e: ArithmeticException) {
- println("Caught ArithmeticException")
- }
- }
結果
- Throwing exception from launch
- Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
- Joined failed job
- Throwing exception from async
- Caught ArithmeticException
總結:
- 協程是可以被取消的和超時控制,可以組合被掛起的函數,協程中運行環境的指定,也就是線程的切換
- 協程最常用的功能是并發,而并發的典型場景就是多線程。
- 協程設計的初衷是為了解決并發問題,讓協作式多任務實現起來更加方便。
- 簡單理解 Kotlin 協程的話,就是封裝好的線程池,也可以理解成一個線程框架。
本文轉載自微信公眾號「 Android開發編程」,可以通過以下二維碼關注。轉載本文請聯系 Android開發編程眾號。