天天看點

Kotlin |協程的了解和使用

文章目錄

      • 協程是什麼?
      • 協程的特性
      • 協程怎麼使用
      • 開始使用協程
      • 案例1:
      • suspend
        • 線程
        • 協程
      • suspend 的挂起
      • suspend 的意義
      • 什麼時候需要自定義 suspend 函數
        • 挂起函數的類型
      • 将回調寫為挂起函數
      • 案例2
      • 非阻塞式挂起
      • 協程的具體使用
        • delay
        • runBlocking
        • 等待一個任務
        • 結構化的并發

協程是什麼?

​ 其實就是一套由 Kotlin 官方提供的線程 API 。可以以非常優雅的方式來執行異步代碼。使用簡單,高效。

協程的特性

  • 結構化并發:
val scope = CoroutineScope(Dispatchers.Main).launch {
    launch {
        launch {

        }
    }

    launch {

    }
}
scope.cacel()
           

協程中支援嵌套,并且具有子協程和父協程的概念

如上,在啟動了一個協程後,在内部有啟動了多個子協程,在子協程中還可以繼續啟動協程。傳回來看線程,線程中可以建立線程,但是他們之間是沒有關聯的

協程的這種特性被稱為結構化并發,這種方式可以讓協程非常友善的管理,比方說關閉協程,隻需要關閉最外面的協程後,内部的協程都會被關閉。對于子協程也可以擷取他的傳回值并調用 cacel 進行關閉。
  • 協程的取消
  • 協稱的異常處理

協程怎麼使用

在項目中配置對 Kotlin 的協程的支援

//核心庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1" 
//依賴目前平台對應的庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1"
           

Kotlin 協程是以官方擴充庫的形式進行支援的。而且 核心庫 和 平台庫的版本應該保持一緻。

  • 核心庫中包含的代碼主要是協程的公共 API 部分,有了這一層的代碼,才使得協程在各個平台上的接口得到統一
  • 平台庫中包含的代碼主要是協程架構在具體平台的具體實作方式。因為多線程在每個平台上都是有差異的。

開始使用協程

//launch 函數的含義:我要建立一個新的協程。并且在指定的線程上運作它
CoroutineScope(Dispatchers.IO).launch {
	println(Thread.currentThread().name)
}
           

可嵌套使用

CoroutineScope.launch(Dispatchers.IO) {
    val image = getImage(imageId)
    launch(Dispatch.Main) { //将會運作在 Main 線程
        avatarIv.setImageBitmap(image)
    }
}
           

如果隻是嵌套,這并沒有多少作用。協程有一個非常好用的函數 :withContext。這個函數可以切換到指定線程,并在閉包中的邏輯執行完後自動把線程切回去繼續執行

CoroutineScope(Dispatchers.Main).launch {
    println(Thread.currentThread().name)
    val bitmap = withContext(Dispatchers.IO) {  //切換到 IO 線程
        getImage()
    }
    iv.setImageBitmap(bitmap)		//主線程更新
}
           

由于可以自動切回來,我們甚至可以把 withContext 放進一個單獨的函數裡面,如下:

suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
    //.....
}
           

但是要注意 suspend 關鍵字。這個關鍵字後面在說,他中文意思是 暫停 或者 可挂起。

案例1:

通過協程下載下傳一張網絡圖檔,并且顯示出來

override fun bindView(view: View) {
        val iv = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv)
        btn = view.findViewById(R.id.delegate_shop_btn)

        btn.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val bitmap = getImage()
                iv.setImageBitmap(bitmap)
            }
        }
    }

private suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
    OkHttpClient().newCall(Request.Builder()              			.url("https://dss0.bdstatic.com/6Ox1bjeh1BF3odCf/it/u=4256581120,3161125441&fm=193")
                           .get()
                           .build())
    .execute().body()?.byteStream().use {
        BitmapFactory.decodeStream(it)
    }
}
           

suspend

協程指的就是 launch 中的代碼,那麼協程中的挂起是什麼呢? 其實挂起的對象就是協程

當執行在 launch 中時,在執行到某一個 suspend 函數時,這個協程就會被挂起。讓

時間靜止,兵分兩路,來看一下到底是怎麼回事,這兩路分别是協程和線程(UI線程)

線程

​ 當代碼執行到協程中的 suspend 函數後,就暫時不會執行協程代碼,而是跳出協程的代碼塊。繼續向下執行。

CoroutineScope(Dispatchers.Main).launch {
    val bitmap = getImage()	//挂起
    iv.setImageBitmap(bitmap)
}
ToastUtils.show("哈哈哈哈")
           

​ 當主線程執行到 getImage 時,會跳出協程,執行下面的 Toast。

​ 這個協程本質上會往主線程 post 一個 Runnable。然後繼續執行協程内部代碼。當執行到被挂起的時候,Runnable 會提前結束,線程繼續執行其他的東西。而協程則會被挂起。是以接下來看一下協程

協程

​ 主線程在執行到 suspend 的時候會被掐斷,接下來協程會繼續往下執行。不過是執行在指定的線程。通過 withContext 傳入的 Dispatchers.IO 所指定的 IO 線程

​ Dispatchers 排程器:将協程限制在一個特定的線程執行,或者将他分派到一個線程池。

​ 日常使用的排程器:Main:Android主線程,IO:網絡IO,Default:适合CPU 密集的任務,比如計算。

​ 協程從 suspend 開始執行在指定的線程,執行完之後,就會自動将我們把線程切回來。

​ 切回來就是切換到原本的線程,如原本是運作在主線程的,切回來後就會繼續在主線程執行。也就是說協程會幫我們 post 一個 Runnable 到主線程。

通過上面兩個角度,可以得到一個解釋:協程在執行到有 suspend 的時候就會被挂起,而這個挂起,則就是切個線程;隻不過挂起的執行完後會重新切回他原來的線程

這個切回來的動作,在 Kotlin 中叫做 resume ,恢複

suspend 的挂起

​ 挂起函數,就是以 suspend 修飾的函數,挂起函數隻能在 其他挂起函數或者協程中使用

​ 這是一個關鍵字。但是他并不是正真的挂起。你可以寫一個帶 supend 的函數,運作後就會發現并沒有挂起,為啥沒有被挂起,應為它不知道往哪切,需要我們來告訴他。如下:

suspend fun get() = withContext(Dispatchers.IO) {
       
    }
           

​ withContext 本身就是一個挂起函數,接收一個

Dispatcher

參數,他必須依賴于這個參數,才能知道協程需要被挂起。接着才會切換到别的線程

​ 是以 suspend 起不到任何挂起函數的作用,挂起函數是 kt 的協程幫我們做的

suspend 的意義

​ 為啥 suspend 關鍵字沒有實際的挂起,但 Kotlin 為啥要把它提供出來?

​ 因為他本來就不是用來操作挂起的。也就是說切線程依賴的是函數中的代碼,而不是這個關鍵字。是以這個關鍵字隻是用來提醒。

​ 如果你建立一個 suspend 函數,但是内部不包含正真的挂起邏輯,編譯器會給你提醒: Redundant ‘suspend’ modifier ,這個關鍵字是多餘的

​ 因為這個函數并不會發生挂起,那這個 suspend 隻有一個效果:限制此函數隻能在協程中被調用,如果在非協程中調用,則編譯不會通過

​ 是以,建立一個 suspend 函數,為了讓他包含挂起,要在内部直接或者間接調用 Kotlin 自帶的 suspend 函數,這個時候函數才是有意義的

什麼時候需要自定義 suspend 函數

​ 如果你某個函數比較耗時,也就是需要等,就可以把它寫成 suspend 函數

​ 給函數加上 suspend 關鍵字,然後在 withContext 把函數内容包住就行了。當然并不是隻有 withContext 來輔助我們實作自定義的函數,如 delay ,他的作用是等一段時間後在繼續往下執行代碼。

suspend fun get() {
	delay(5) //挂起
}
           

挂起函數的類型

suspend fun foo(){}
           

如果沒有 suspend 這個函數的類型就是 ()-> Unit

但是加了 suspend 後,函數的類型為 suspend()-> Unit

隻要你知道函數的類型是什麼,然後在前面加一個 suspend 就是挂起函數的類型

suspend fun bar(a:Int):String{
        return "Hello"
}
           

挂起函數 bar 的類型為 suspend(Int)->String

還記得 suspend 為什麼隻能在 挂起函數或者協程中調用嗎?

​ 因為所有的挂起函數都有一個 Continuation 參數,Continuation 是從哪來的呢,suspend 關鍵字會隐含在 函數的參數清單的最後加一個 Continuation 參數。Continuation 的泛型參數是由函數的傳回值來決定的

Kotlin |協程的了解和使用

上面的 挂起函數 bar 最終的樣子如上圖,他會在參數清單的最後加一個參數,并且傳回值成為了 Any。這個 Any 有兩種情況,如果這個函數沒有真正的挂起,比如 bar函數,foo 函數。在函數沒有真正挂起的時候,這個 Any就是用來承載傳回值結果。如果函數真的被挂起了,這個Any 傳回的是一個挂起的标志 COROUTINE_SUSPENDED ,讓外部的協程體知道我這個協程真正被挂起了。要等待這個函數的回調,才能繼續往下執行。 是以這個 Any 是非常重要的

将回調寫為挂起函數

​ 通過 suspendCoroutine 來實作:

private suspend fun getImage() = suspendCoroutine<Bitmap> { 
        continuation ->
        OkHttpClient().newCall(Request.Builder()
                .url("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1581869591580&di=e0412feb1e101a144e416f7a873bd88d&imgtype=0&src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2F6febb183087736d089b6583a790c491f2dc7469a.jpg")
                .get()
                .build())
                .enqueue(object : Callback {
                    override fun onFailure(call: Call, e: IOException) {
                        continuation.resumeWithException(e)
                    }
                    override fun onResponse(call: Call, response: Response) {
                        continuation.resume(response.body()?.byteStream().use { BitmapFactory.decodeStream(it)})
                    }
                })
    }
           

​ 如果要将回調轉為挂起,需要使用 suspendCoroutine 這個函數調用擷取目前函數的 Continuation。通過這個方法就可以拿得到 Continuation。這個參數被編譯器藏了起來。

​ 回調成功可以使用 resume 或者 resumeWith 将結果傳回

​ 異常就是用 resumeWithException 即可。

案例2

​ 網絡請求一張圖檔,并進行兩次切割 。1,切成四份,取第一份。2,切9份,去最後一份

override fun bindView(view: View) {
        //        getSupportDelegate().loadRootFragment(R.id.delegate_shop_layout,
        //                BaseShopListDelegate.newInstance(CarPreference.getMyCar(), BusinessScope.BUSINESSSCOPE_SHOP_LIST));

        val iv1 = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv1)
        val iv2 = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv2)
        val iv3 = view.findViewById<AppCompatImageView>(R.id.delegate_shop_iv3)
        btn = view.findViewById(R.id.delegate_shop_btn)

        btn.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val bitmap = getImage()
                iv1.setImageBitmap(bitmap)//原圖
                val bm1 = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width / 2, bitmap.height / 2)
                iv2.setImageBitmap(bm1)
                val bm2 = Bitmap.createBitmap(bitmap, bitmap.width / 3 * 2, bitmap.height / 3 * 2, bitmap.width / 3, bitmap.height / 3)
                iv3.setImageBitmap(bm2)
            }
        }
    }

    private suspend fun getImage(): Bitmap = withContext(Dispatchers.IO) {
        OkHttpClient().newCall(Request.Builder()
                .url("https://dss0.bdstatic.com/6Ox1bjeh1BF3odCf/it/u=4256581120,3161125441&fm=193")
                .get()
                .build())
                .execute().body()?.byteStream().use {
                    BitmapFactory.decodeStream(it)
                }
    }
           

非阻塞式挂起

​ 首先什麼是阻塞呢?

1,前面有障礙物,過不去了(線程卡主)

2,清除障礙物(等待耗時任務結束)

3,繞道而行(切到别的線程)

​ 非阻塞式挂起并沒有限定在一個線程中,因為挂起本來就涉及到多個線程。主線程執行的時候遇到耗時任務,然後将耗時任務挂起,這時主線程就自由了,可以繼續做别的事了。是以非阻塞式挂起其實就是在講 協程在挂起的時候切換線程這件事。

​ 協程隻是看起來會阻塞,但其實是非阻塞的,因為它可以切線程

​ 協程與線程:在 Kotlin 中,協程就是基于線程實作的一種更上層的工具 API ,隻不過他的用法非常簡單。

​ 協程是什麼:基于線程的一個架構

​ 協程的挂起:自動切換線程

​ 非阻塞式挂起:可以用看起來阻塞的代碼來實作非阻塞的操作

協程的具體使用

delay

fun main() {
    GlobalScope.launch {
        delay(1000L) //協程挂起,阻塞1秒
        println(" 2020")
    }
    print("hello")  //協程挂起時,主線程繼續執行
    Thread.sleep(2000L)//延時:保證 主線程存活
}
//hello 2020
           

runBlocking

​ 在講建立協程的時候說過,runBlocking 是阻塞式的

fun main() {
    GlobalScope.launch {
        delay(1000L)
        println("2020 ")
    }
    println("Hello ")
    //表達式阻塞了主線程,調用了 runBlocking 的主線程會一直等到 runBlocking執行完畢
    runBlocking {
        delay(2000L) //延時:保證 主線程存活
    }
}
//Hello 2020 
           

​ 修改代碼如下:

fun main() = runBlocking {//開始執行主協程
    println(Thread.currentThread().name)
    GlobalScope.launch {
        delay(1000L)
        println("2020 ")
    }
    print("Hello ")
    delay(2000L) //延時:保證 主線程存活
}
//main
//Hello 2020 
           

​ 其實還是主線程

等待一個任務

suspend fun main() {
    val job = GlobalScope.launch {
        delay(1000L)
        println(" 2020")
    }
    println("Hello")
    job.join() //等待子線程執行結束
}
           

​ 注意 main 方法被 suspend 修飾了,因為 join 方法被 suspend 修飾過,suspend 本身不會挂起,挂起是因為 join 内部有挂起的代碼。suspend 隻是一個提示。隻是這個提示必須寫。

結構化的并發

fun main() = runBlocking {
    //開始執行主協程
    launch {
        delay(1000L)
        println(" 2020")
    }
    print("Hello")
}
//Hello 2020
           

​ 注意:在最後并沒有讓主線程等待,也沒有調用 join。為啥會列印出 2020 呢?

​ 在 runBlocking 内的每個協程建構器中都将 CoruntineScope 的執行個體添加到代碼塊的作用域中。我們可以在這個作用域中啟動協程而無需顯示調用 join。因為外部協程(runBlocking)直到在其作用域中啟動的所有協程執行完畢後才會結束。

參考自慕課網視訊

參考自扔物線

參考自官網