談談我對Kotlin中協程的理解

1 協程(Coroutines)是什麼

kotlin 官方文檔說:本質上,協程是輕量級的線程。

從 Android 開發者的角度去理解它們的關係:

  • 我們所有的代碼都是跑在線程中的,而線程是跑在進程中的。
  • 協程沒有直接和操作系統關聯,但它不是空中樓閣,它也是跑在線程中的,可以是單線程,也可以是多線程。
  • 單線程中的協程總的執行時間並不會比不用協程少。
  • Android 系統上,如果在主線程進行網絡請求,會拋出 NetworkOnMainThreadException,對於在主線程上的協程也不例外,這種場景使用協程還是要切線程的。

我們學習Kotlin 中的協程,一開始確實可以從線程控制的角度來切入。因爲在 Kotlin 中,協程的一個典型的使用場景就是線程控制。就像 Java 中的 Executor 和 Android 中的 AsyncTaskKotlin 中的協程也有對 Thread API 的封裝,讓我們可以在寫代碼時,不用關注多線程就能夠很方便地寫出併發操作。

小結:

  • 協程最常用的功能是併發,而併發的典型場景就是多線程。

  • 協程設計的初衷是爲了解決併發問題,讓 協作式多任務實現起來更加方便。

  • 簡單理解 Kotlin 協程的話,就是封裝好的線程池,也可以理解成一個線程框架。

  • 那麼Kotlin中的協程是通過什麼來實現異步操作的呢?它使用的是一種叫做 掛起 的機制。

2 你需要用協程嗎?

在這裏插入圖片描述

RxJava 可以解決回調問題,同樣我們可以用協程解決回調問題。

3 使用協程優點

  • 輕量級,佔用更少的系統資源;

  • 更高的執行效率;

  • 掛起函數較於實現Runnable或Callable接口更加方便可控;

  • kotlin.coroutine 核心庫的支持,讓編寫異步代碼更加簡單。

4 kotlin協程的演進

在這裏插入圖片描述
解釋說明:

  • Job: 任務,封裝了協程中需要執行的代碼邏輯。Job 可以取消並且有簡單生命週期
  • Coroutine context:協程上下文,協程上下文裏是各種元素的集合
  • Coroutine dispatchers :協程調度,可以指定協程運行在 Android 的哪個線程裏
  • suspend:掛起函數。掛起,就是一個稍後會被自動切回來的線程調度操作。

5 實現方式

5.1 環境準備

  • Kotlin 版本: 1.3.+
  • 依賴的框架:在 app/build.gradle 裏添加 Kotlin 協程庫的依賴如下所示。
//kotlin 標準庫
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

//依賴協程核心庫 ,提供Android UI調度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1"

//依賴當前平臺所對應的平臺庫 (必須)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
    

5.2 創建協程的幾種方式

方式 作用
launch:job 創建一個不會阻塞當前線程、沒有返回結果的 Coroutine,但會返回一個 Job 對象,可以用於控制這個 Coroutine 的執行和取消,返回值爲Job。
runBlocking:T 創建一個會阻塞當前線程的Coroutine,常用於單元測試的場景,開發中一般不會用到
async/await:Deferred async 返回的 Coroutine 多實現了 Deferred 接口,簡單理解爲帶返回值的launch函數

實現方式一:GlobalScope.launch,使用 GlobalScope 單例對象, 可以直接調用 launch 開啓協程。

  override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_thread)
        loadData()
    }

	private fun loadData() {
        GlobalScope.launch(Dispatchers.IO) { //在IO線程開始
            //IO 線程里拉取數據
            val result = fetchData()
            //主線程裏更新 UI
            withContext(Dispatchers.Main) { //執行結束後,自動切換到UI線程
                tvShowContent.text = result
            }
        }
    }
	
	//關鍵詞 suspend
    private suspend fun fetchData(): String {
        delay(2000) // delaying for 2 seconds to keep JVM alive
        return "content"
    }

我們最常用的用於啓動協程的方式,它最終返回一個Job類型的對象,這個Job類型的對象實際上是一個接口,它包涵了許多我們常用的方法。 該方式啓動的協程任務是不會阻塞線程的*

實現方式二:使用 runBlocking 頂層函數

runBlocking {}是創建一個新的協程同時阻塞當前線程,直到協程結束。這個不應該在協程中使用,主要是爲main函數和測試設計的 。

fun main(args: Array<String>) = runBlocking { // start main coroutine
    launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive
}

實現方式三:async+await

    private fun testAysnc() = GlobalScope.launch {
        val deferred = async(Dispatchers.IO) {
            delay(3000L)
            "Show Time"
        }
        // 此處獲取耗時任務的結果,我們掛起當前協程,並等待結果
        val result = deferred.await()

        //掛起協程切換至UI線程 展示結果
        withContext(Dispatchers.Main) {
            tvShowContent.text = result
        }
    }
  • async和await是兩個函數,這兩個函數在我們使用過程中一般都是成對出現的。

  • async用於啓動一個異步的協程任務,await用於去得到協程任務結束時返回的結果,結果是通過一個Deferred對象返回的。

那我們平日裏常用到的調度器有哪些?

Dispatchers種類 作用
Dispatchers.Default 共享後臺線程池裏的線程(適合 CPU 密集型的任務,比如計算)
Dispatchers.Main Android中的主線程
Dispatchers.IO 共享後臺線程池裏的線程(針對磁盤和網絡 IO 進行了優化,適合 IO 密集型的任務,比如:讀寫文件,操作數據庫以及網絡請求)
Dispatchers.Unconfined 不限制,使用父Coroutine的現場

回到我們的協程,它從 suspend 函數開始脫離啓動它的線程,繼續執行在 Dispatchers 所指定的 IO 線程。

緊接着在 suspend 函數執行完成之後,協程爲我們做的最爽的事就來了:會自動幫我們把線程再切回來

這個"切回來"是什麼意思?

我們的協程原本是運行在主線程的,當代碼遇到 suspend 函數的時候,發生線程切換,根據 Dispatchers 切換到了 IO 線程;

當這個函數執行完畢後,線程又切了回來,"切回來"也就是協程會幫我再 post 一個 Runnable,讓我剩下的代碼繼續回到主線程去執行。

6 協程的應用場景

6.1 從相冊中讀取圖片並顯示

從相冊中直接讀取圖片,這是一個典型的IO操作使用場景,操作不當,可能會出現ANR。

版本1.0實現方式

val mImageUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, mImageUri)
imageView.setImageBitmap(bitmap)

版本2.0 我們可能會引入HandlerAysnTask來通過異步的方式實現

版本3.0 我們可以這樣用doAsync實現 這種方式也不錯

doAsync{
    //後臺執行
   val mImageUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI
   val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,mImageUri) 
    
    //回到主線程
    uiThread{
       imageView.setImageBitmap(bitmap)
    }
}

版本4.0 時我們就可以用協程來實現。


val job = launch(Background) {
   val mImageUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI
   val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,mImageUri) 
   launch(UI) {
    imageView.setImageBitmap(bitmap)
  }

這裏的參數Background是一個CoroutineContext對象,確保這個協程運行在一個後臺線程,確保你的應用程序不會因耗時操作而阻塞和崩潰。你可以像下邊這樣定義一個CoroutineContext:

internal val Background = newFixedThreadPoolContext(2, "bg")

人個感覺 最後兩種方式都可取。

6.2 Android Jetpack 中使用 kotlin 協程

後面介紹的三種使用方式在實現前需要分別添加以下的依賴包

    implementation  'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc02'

    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc02'

    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc02'

6.2.1在ViewModel中使用ViewModelScope

爲應用程序中的每個ViewModel定義ViewModelScope。如果清除ViewModel,則在此作用域中啓動的任何協同程序都將自動取消。

當只有在ViewModel處於活動狀態時才需要完成工作時,協程在這裏非常有用。

例如,如果要爲佈局計算某些數據,則應將工作範圍設置爲ViewModel,以便在清除ViewModel時,自動取消工作以避免消耗資源。

可以通過ViewModel的viewModelScope屬性訪問ViewModel的協同作用域,如下例所示:


class MyViewModel :ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

6.2.2 在Activity或Fragment中使用LifecycleScope

爲每個Lifecycle定義LifecycleScope。當 Lifecycle 銷燬時,在此範圍內啓動的任何協同程序都將被取消。

您可以通過Lifecycle.CoroutineScopelifecycleOwner.lifecycleScope屬性訪問LifecycleCoroutineScope

下面的示例演示如何使用lifecycleOwner.lifecycleScope異步創建預計算文本:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

6.2.3 使用LiveData

使用LiveData時,可能需要異步計算值。例如,您可能希望檢索用戶的首選項並將其提供給您的UI。在這些情況下,可以使用liveData builder函數調用suspend函數,將結果作爲liveData對象提供。

在下面的示例中,loadUser()是在別處聲明的掛起函數。使用liveData 構建函數異步調用loadUser(),然後使用emit()發出結果。

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

LiveData構建塊充當協同路由和liveData之間的結構化併發原語。代碼塊在LiveData變爲活動時開始執行,並且在LiveData變爲非活動時經過可配置的超時後自動取消。如果在完成之前取消,則在LiveData再次激活時重新啓動。如果在上一次運行中成功完成,則不會重新啓動。請注意,只有在自動取消時纔會重新啓動。如果由於任何其他原因(例如拋出異常CancelationException)而取消塊,則不會重新啓動它。

也可以從塊中發射多個值。每次emit()調用都會暫停塊的執行,直到在主線程上設置LiveData值。

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

我們也可以和 LifeCycle中的Transformations結合使用,如下例所示:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

6.3 在Retofit 使用kotlin協程

retrofit 2.6.0(2019-06-05)中的更新日誌如下:

Support suspend modifier on functions for Kotlin! This allows you to express the asynchrony of HTTP requests in an idiomatic fashion for the language.

@GET("users/{id}")
suspend fun user(@Path("id") id: Long): User

Behind the scenes this behaves as if defined as fun user(...): Call and then invoked with Call.enqueue. You can also return Response for access to the response metadata.

在函數前加上 suspend 函數直接返回你需要對象類型不需要返回Call對象

總結

本文總結了kotlin中的協程的相關知識點,協程是值得深入研究的。 未來的項目中運用是趨勢所在,現將學習的心得總結於此,方便未來迭代中做爲技術的儲備。如有不足之處,歡迎留言討論。

參考資料:

1.Google官網在component中協程的運用

2.小慕帶你學習Kotlin之協程

3.Kotlin 的協程用力瞥一眼 - 學不會協程?很可能因爲你看過的教程都是錯的

4.Kotlin協程的使用

5.【碼上開學】Kotlin 協程的掛起好神奇好難懂?今天我把它的皮給扒了

6.kotlin 協程在 Android 中的使用(Jetpack 中的協程、Retofit中使用協程)

7.Kotlin協程 —— 今天說說 launch 與 async

發佈了50 篇原創文章 · 獲贊 192 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章