Kotlin協程

什么是協程?

官方描述:協程通過將復雜性放入庫來簡化異步編程。程序的邏輯可以在協程中順序地表達,而底層庫會為我們解決其異步性。該庫可以將用戶代碼的相關部分包裝為回調、訂閱相關事件、在不同線程(甚至不同機器)上調度執行,而代碼則保持如同順序執行一樣簡單。

協程就像非常輕量級的線程。線程是由系統調度的,線程切換或線程阻塞的開銷都比較大。而協程依賴于線程,但是協程掛起時不需要阻塞線程,幾乎是無代價的,協程是由開發者控制的。所以協程也像用戶態的線程,非常輕量級,一個線程中可以創建任意個協程。

協程很重要的一點就是當它掛起的時候,它不會阻塞其他線程。協程底層庫也是異步處理阻塞任務,但是這些復雜的操作被底層庫封裝起來,協程代碼的程序流是順序的,不再需要一堆的回調函數,就像同步代碼一樣,也便于理解、調試和開發。它是可控的,線程的執行和結束是由操作系統調度的,而協程可以手動控制它的執行和結束。

使用

首先需要添加依賴:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"

1.runBlocking:T

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.launch:Job

這是最常用的用于啟動協程的方式,它最終返回一個Job類型的對象,這個Job類型的對象實際上是一個接口,它包涵了許多我們常用的方法。下面先看一下簡單的使用:

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方法的定義:

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

1.協程下上文

上下文可以有很多作用,包括攜帶參數,攔截協程執行等等,多數情況下我們不需要自己去實現上下文,只需要使用現成的就好。上下文有一個重要的作用就是線程切換Kotlin協程使用調度器來確定哪些線程用于協程執行,Kotlin提供了調度器給我們使用:

  • Dispatchers.Main:使用這個調度器在 Android 主線程上運行一個協程。可以用來更新UI 。在UI線程中執行

  • Dispatchers.IO:這個調度器被優化在主線程之外執行磁盤或網絡 I/O。在線程池中執行

  • Dispatchers.Default:這個調度器經過優化,可以在主線程之外執行 cpu 密集型的工作。例如對列表進行排序和解析 JSON。在線程池中執行

  • Dispatchers.Unconfined:在調用的線程直接執行。

調度器實現了CoroutineContext接口

2.啟動模式

Kotlin協程當中,啟動模式定義在一個枚舉類中:

public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    @ExperimentalCoroutinesApi
    ATOMIC,
    @ExperimentalCoroutinesApi
    UNDISPATCHED;
}

一共定義了4種啟動模式,下表是含義介紹:

啟動模式 作用
DEFAULT 默認的模式,立即執行協程體
LAZY 只有在需要的情況下運行
ATOMIC 立即執行協程體,但在開始運行之前無法取消
UNDISPATCHED 立即在當前線程執行協程體,直到第一個 suspend 調用

2.協程體

協程體是一個用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

asynclaunch的用法基本一樣,區別在于:async的返回值是Deferred,將最后一個封裝成了該對象。async可以支持并發,此時一般都跟await一起使用,看下面的例子。

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是不阻塞線程的,也就是說getResult1getResult2是同時進行的,所以獲取到result的時間是4s,而不是7s。

應用

項目中的網絡請求框架大部分都是基于RxJava + Retrofit + Okhttp封裝的,RxJava可是很好的實現線程之間的切換,如果只是網絡框架中用到了RxJava,那就是“大材小用”了,畢竟RxJava的功能還是很強大的。Retrofit2.6.0開始已經支持協程了:可以定義成一個掛起函數。

interface Api {
    @POST("user/login")
    suspend fun login(): Call<User>
}

下面的例子是使用協程來代替RxJava實現線程切換。

1.首先定義一個請求相關的支持DSL語法的接收者。
class RetrofitCoroutineDSL<T> {

    var api: (Call<Result<T>>)? = null
    internal var onSuccess: ((T) -> Unit)? = null
        private set
    internal var onFail: ((msg: String, errorCode: Int) -> Unit)? = null
        private set
    internal var onComplete: (() -> Unit)? = null
        private set

    /**
     * 獲取數據成功
     * @param block (T) -> Unit
     */
    fun onSuccess(block: (T) -> Unit) {
        this.onSuccess = block
    }

    /**
     * 獲取數據失敗
     * @param block (msg: String, errorCode: Int) -> Unit
     */
    fun onFail(block: (msg: String, errorCode: Int) -> Unit) {
        this.onFail = block
    }

    /**
     * 訪問完成
     * @param block () -> Unit
     */
    fun onComplete(block: () -> Unit) {
        this.onComplete = block
    }

    internal fun clean() {
        onSuccess = null
        onComplete = null
        onFail = null
    }
}
2.然后給協程定義一個擴展方法,用于Retrofit網絡請求。
fun <T> CoroutineScope.retrofit(dsl: RetrofitCoroutineDSL<T>.() -> Unit) {
    //在主線程中開啟協程
    this.launch(Dispatchers.Main) {
        val coroutine = RetrofitCoroutineDSL<T>().apply(dsl)
        coroutine.api?.let { call ->
            //async 并發執行 在IO線程中
            val deferred = async(Dispatchers.IO) {
                try {
                    call.execute() //已經在io線程中了,所以調用Retrofit的同步方法
                } catch (e: ConnectException) {
                    coroutine.onFail?.invoke("網絡連接出錯", -1)
                    null
                } catch (e: IOException) {
                    coroutine.onFail?.invoke("未知網絡錯誤", -1)
                    null
                }
            }
            //當協程取消的時候,取消網絡請求
            deferred.invokeOnCompletion {
                if (deferred.isCancelled) {
                    call.cancel()
                    coroutine.clean()
                }
            }
            //await 等待異步執行的結果
            val response = deferred.await()
            if (response == null) {
                coroutine.onFail?.invoke("返回為空", -1)
            } else {
                response.let {
                    if (response.isSuccessful) {
                        //訪問接口成功
                        if (response.body()?.status == 1) {
                            //判斷status 為1 表示獲取數據成功
                            coroutine.onSuccess?.invoke(response.body()!!.data)
                        } else {
                            coroutine.onFail?.invoke(response.body()?.msg ?: "返回數據為空", response.code())
                        }
                    } else {
                        coroutine.onFail?.invoke(response.errorBody().toString(), response.code())
                    }
                }
            }
            coroutine.onComplete?.invoke()
        }
    }
}

在上面的代碼中,比較難理解的是下面的代碼:

val coroutine = RetrofitCoroutineDSL<T>().apply(dsl)

dsl是帶接收者的函數字面量,接收者是RetrofitCoroutineDSL,所有先創建一個接受者對象,然后將傳入的實參dsl賦值給該對象。還可以寫成下面的樣子:

val coroutine = RetrofitCoroutineDsl<T>()
coroutine.dsl() 

上面的寫法是直接調用函數字面量。為了方便里面,把上述代碼翻譯成對應的Java代碼:

RetrofitCoroutineDsl<T> coroutine = new RetrofitCoroutineDsl<T>();
dsl.invoke(coroutine);

調用函數dsl并傳入coroutine,其實就是把dsl賦值給coroutine

3.最后一步,讓BaseActivity實現接口CoroutineScope,這樣在頁面中的上下文就是協程下上文
open class BaseActivity : AppCompatActivity(), CoroutineScope {

    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 關閉頁面后,結束所有協程任務
        job.cancel() 
    }
}

+CoroutineContext中的運算符重載,包含兩者的上下文:

//Returns a context containing elements from this context and elements from  other [context].
//The elements from this context with the same key as in the other one are dropped.
public operator fun plus(context: CoroutineContext): CoroutineContext

Activity中可以直接調用擴展函數retrofit來調用網絡請求:

retrofit<User> {
    api = RetrofitCreater.create(Api::class.java).login()
    onSuccess {
        Log.e(TAG, "result = ${it?.avatar}")
    }
    onFailed { msg, _ ->
        Log.e(TAG, "onFailed = $msg")
    }
}

如果不需要處理訪問失敗的情況,可以寫成下面的樣子:

retrofit<User> {
    api = RetrofitCreater.create(Api::class.java).login()
    onSuccess {
        Log.e(TAG, "result = ${it?.avatar}")
    }
}

使用協程可以更好的控制任務的執行,并且比線程更加的節省資源,更加的高效。結合DSL的代碼風格,可以讓我們的程序更加直觀易懂、簡潔優雅。

Kotlin協程原理詳解

Kotlin DSL

Kotlin實戰

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 輕量級線程:協程 在常用的并發模型中,多進程、多線程、分布式是最普遍的,不過近些年來逐漸有一些語言以first-c...
    Tenderness4閱讀 6,390評論 2 10
  • 你的第一個協程 輸出結果 從本質上講,協同程序是輕量級的線程。它們是與發布 協同程序構建器一起啟動的。您可以實現相...
    十方天儀君閱讀 2,548評論 0 2
  • 前言 ?今年的Google開發者大會已表明將Kotlin作為其正式的語言,現Google大力主推Kotlin, 在...
    Vgecanshang閱讀 3,512評論 0 15
  • 協程是一種并發設計模式,你可以在 Android 上使用它來簡化異步代碼。協程是在 Kotlin 1.3 時正式發...
    Android高級工程師閱讀 2,653評論 0 4
  • 今天分享這一本《小金魚逃走了》 這本書我在豆豆11個月大的時候開始講給他聽,他還不會說話,閱讀中跟隨故事情節,當我...
    郭薛玲閱讀 308評論 2 1