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

runBlocking啟動的協(xié)程任務會阻斷當前線程,直到該協(xié)程執(zhí)行結(jié)束。當協(xié)程執(zhí)行結(jié)束之后,頁面才會被顯示出來。
2、GlobalScope.launch{}
在應用范圍內(nèi)啟動一個新協(xié)程,協(xié)程的生命周期與應用程序一致。這樣啟動的協(xié)程并不能使線程?;?,就像守護線程。
由于這樣啟動的協(xié)程存在啟動協(xié)程的組件已被銷毀但協(xié)程還存在的情況,極限情況下可能導致資源耗盡,因此并不推薦這樣啟動,尤其是在客戶端這種需要頻繁創(chuàng)建銷毀組件的場景。
- 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, "協(xié)程執(zhí)行結(jié)束 -- 線程id:${Thread.currentThread().id}")
- }
- Log.e(TAG, "主線程執(zhí)行結(jié)束")
- }
- //Job中的方法
- job.isActive
- job.isCancelled
- job.isCompleted
- job.cancel()
- jon.join()
從執(zhí)行結(jié)果看出,launch不會阻斷主線程。
下面我們來總結(jié)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的一個擴展函數(shù),CoroutineScope簡單來說就是協(xié)程的作用范圍。launch方法有三個參數(shù):1.協(xié)程下上文;2.協(xié)程啟動模式;3.協(xié)程體:block是一個帶接收者的函數(shù)字面量,接收者是CoroutineScope
①.協(xié)程下上文
- 上下文可以有很多作用,包括攜帶參數(shù),攔截協(xié)程執(zhí)行等等,多數(shù)情況下我們不需要自己去實現(xiàn)上下文,只需要使用現(xiàn)成的就好。上下文有一個重要的作用就是線程切換,Kotlin協(xié)程使用調(diào)度器來確定哪些線程用于協(xié)程執(zhí)行,Kotlin提供了調(diào)度器給我們使用:
- Dispatchers.Main:使用這個調(diào)度器在 Android 主線程上運行一個協(xié)程??梢杂脕砀耈I 。在UI線程中執(zhí)行
- Dispatchers.IO:這個調(diào)度器被優(yōu)化在主線程之外執(zhí)行磁盤或網(wǎng)絡 I/O。在線程池中執(zhí)行
- Dispatchers.Default:這個調(diào)度器經(jīng)過優(yōu)化,可以在主線程之外執(zhí)行 cpu 密集型的工作。例如對列表進行排序和解析 JSON。在線程池中執(zhí)行。
- Dispatchers.Unconfined:在調(diào)用的線程直接執(zhí)行。
- 調(diào)度器實現(xiàn)了CoroutineContext接口。
②.啟動模式
在Kotlin協(xié)程當中,啟動模式定義在一個枚舉類中:
- public enum class CoroutineStart {
- DEFAULT,
- LAZY,
- @ExperimentalCoroutinesApi
- ATOMIC,
- @ExperimentalCoroutinesApi
- UNDISPATCHED;
- }
一共定義了4種啟動模式,下表是含義介紹:
- DEFAULT:默認的模式,立即執(zhí)行協(xié)程體
- LAZY:只有在需要的情況下運行
- ATOMIC:立即執(zhí)行協(xié)程體,但在開始運行之前無法取消
- UNDISPATCHED:立即在當前線程執(zhí)行協(xié)程體,直到第一個 suspend 調(diào)用
③.協(xié)程體
協(xié)程體是一個用suspend關鍵字修飾的一個無參,無返回值的函數(shù)類型。被suspend修飾的函數(shù)稱為掛起函數(shù),與之對應的是關鍵字resume(恢復),注意:掛起函數(shù)只能在協(xié)程中和其他掛起函數(shù)中調(diào)用,不能在其他地方使用。
suspend函數(shù)會將整個協(xié)程掛起,而不僅僅是這個suspend函數(shù),也就是說一個協(xié)程中有多個掛起函數(shù)時,它們是順序執(zhí)行的??聪旅娴拇a示例:
- 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,"主線程執(zhí)行$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方法將協(xié)程掛起,協(xié)程中其后面的代碼永遠不會執(zhí)行,只有等到getToken掛起結(jié)束恢復后才會執(zhí)行。同時協(xié)程掛起后不會阻塞其他線程的執(zhí)行。
3.async/await:Deferred
async跟launch的用法基本一樣,區(qū)別在于:async的返回值是Deferred,將最后一個封裝成了該對象。async可以支持并發(fā),此時一般都跟await一起使用,看下面的例子。
async和await是兩個函數(shù),這兩個函數(shù)在我們使用過程中一般都是成對出現(xiàn)的;
async用于啟動一個異步的協(xié)程任務,await用于去得到協(xié)程任務結(jié)束時返回的結(jié)果,結(jié)果是通過一個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。
三、協(xié)程異常
1、因協(xié)程取消,協(xié)程內(nèi)部suspend方法拋出的CancellationException
2、常規(guī)異常,這類異常,有兩種異常傳播機制
- launch:將異常自動向父協(xié)程拋出,將會導致父協(xié)程退出
- 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() // 沒有打印任何東西,依賴用戶去調(diào)用等待
- }
- try {
- deferred.await()
- println("Unreached")
- } catch (e: ArithmeticException) {
- println("Caught ArithmeticException")
- }
- }
結(jié)果
- Throwing exception from launch
- Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
- Joined failed job
- Throwing exception from async
- Caught ArithmeticException
總結(jié):
- 協(xié)程是可以被取消的和超時控制,可以組合被掛起的函數(shù),協(xié)程中運行環(huán)境的指定,也就是線程的切換
- 協(xié)程最常用的功能是并發(fā),而并發(fā)的典型場景就是多線程。
- 協(xié)程設計的初衷是為了解決并發(fā)問題,讓協(xié)作式多任務實現(xiàn)起來更加方便。
- 簡單理解 Kotlin 協(xié)程的話,就是封裝好的線程池,也可以理解成一個線程框架。
本文轉(zhuǎn)載自微信公眾號「 Android開發(fā)編程」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系 Android開發(fā)編程眾號。