快速上手 Kotlin 開發系列之什麼是協程

站在巨人的肩膀上做個筆記,摘錄自:https://kaixue.io/kotlin-coroutines-1

協程是什麼

協程的概念並沒有官方的或者統一的定義,協程原本是一個跟線程非常類似的用於處理多任務的概念,是一種編程思想,並不侷限於特定的語言。

那在 Kotlin 中的協程是什麼呢?

其實就是一套有 Kotlin 官方提供的線程 API。就像 Java 的 Executor 和 Android 的 AsyncTask,Kotlin 協程也對 Thread 相關的 API 做了一套封裝,讓我們不用過多關心線程也可以方便地寫出併發操作,這就是 Kotlin 的協程。

協程的好處

既然 Java 有了 Executor,Android 又有了 Handler 和 AsyncTask 來解決線程間通信,而且現在還有 RxJava,那用協程幹嘛呢?協程好在哪兒呢?

協程的好處,本質上跟其他的線程 API 一樣,都是爲了方便使用,但由於它藉助了 Kotlin 的語言優勢所以比起其他的實現方案要更方便一點。

協程最基本的功能就是併發也就是多線程,用協程你可以把任務切到後臺執行:

launch(Dispatchers.IO) {
  //耗時
}

切到前臺:


launch(Dispatchers.Main) {
   //更新 UI
}

這種寫法很簡單,但它不能算是協程直接使用 Thread 的優勢,因爲 Kotlin 專門添加了一個函數來簡化對線程的直接使用:

thread {
    ...
}

而 Kotlin 最大的好處是在於你可以把運行在不同線程的代碼,寫在同一個代碼塊裏,上下兩行代碼,線程切走再切回來,這是在寫 Java 時絕對做不到的。它可以用看起來同步的方式寫出異步代碼,這就是 Kotlin 最有名的 非阻塞式掛起。例如:

CoroutineScope(Dispatchers.Main).launch {
    val bitmap = suspendingGetBitmap()// 網絡請求,後臺線程
    imageView.setImageBitmap(bitmap)// 更新 UI 主線程
}

Java 中我們處理這樣的場景需要使用回調來解決,一旦邏輯複雜很容易會出現兩層或 n 層回調,這就陷入了回調地獄

而 Kotlin 的協程就完全消除了回調!另外大家不要忘了,回調式可不止多了幾個縮進那麼簡單,它也限制了我們的能力。

比如,我們有個需求,他需要分別執行兩次網絡請求,然後把結果合併後再展示到界面,按照正常思路這兩個接口應該同時發起請求,然後把結果做融合。

但是這種場景如果使用 Java 的回調式就會比較喫力,可能會把兩個接口做串行的請求,但這很明顯是垃圾代碼!(暫時先不考慮 RxJava)

而使用協程可輕鬆實現,依然是上下兩行代碼:

launch(Dispatchers.Main) {
    val avatar = async { getAvatar() }//獲取用戶頭像
    val logo = async { getLogo() }//獲取 Logo
    mergeShowUI(avatar.await(), logo.await())//合併展示
}

所以由於 Kotlin 消除了併發任務之間協作的難度,協程可以讓我們輕鬆的寫出複雜的併發代碼。這些就是協程優勢所在!

async 會在後面的章節中介紹。

協程的基本使用

配置協程

要想在 Android 工程中使用 Kotlin 協程,需要配置相應的依賴:

根目錄下的 build.gradle 配置版本:

buildscript {
    ...
    ext.kotlin_coroutines = '1.3.1'
    ...
}

app 下的 build.gradle:

dependencies {
    ...
    //                                        👇 依賴協程核心庫
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
    //                                        👇 依賴當前平臺所對應的平臺庫
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
    ...
}

創建協程

上面提到的 launch 函數不是一個頂層函數,是不能直接使用的,可以通過下面的三種方法來創建:

// 方法一,使用 runBlocking 頂層函數
runBlocking {
    getImage(imageId)
}

// 方法二,使用 GlobalScope 單例對象
//            👇 可以直接調用 launch 開啓協程
GlobalScope.launch {
    getImage(imageId)
}

// 方法三,自行通過 CoroutineContext 創建一個 CoroutineScope 對象
//                                    👇 需要一個類型爲 CoroutineContext 的參數
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    getImage(imageId)
}
  • 方法一通常用於單元測試場景,業務開發一般不會用它,因爲它是線程阻塞的;
  • 方法二和方法一的區別在於不會線程阻塞。但在 Android 中並不推薦這種用法,因爲它的生命週期與 Application 一致,且不可取消;
  • 方法三爲推薦方式,可以通過傳入的 context 參數來管理協程的生命週期(注:這裏的 context 和 Android 裏的不是一個)

使用協程

接下來開始介紹協程的具體使用,最簡單的方式上面已經介紹了:

launch(Dispatchers.IO) {
    ...
    getImage(imageId)
    ...
}

這個 launch 函數表示含義就是我要創建一個新的協程,並在指定的線程上運行它,這個被創建的協程是誰?就是你傳給 launch 的那些代碼:

...
getImage(imageId)
...

這段連續的代碼就是協程。

所以我們就清楚了什麼時候用協程,就是當你切線程或者指定線程的時候

例如,子線程中獲取圖片,主線程中更新 UI:

launch(Dispatchers.IO) {
    val image = getImage(imageId)
    launch(Dispatchers.Main) {
        iv.setImageBitmap(image)
    }
}

???什麼情況,還是回調地獄???

如果只用 launch 函數協程並不比直接使用線程更加方便,但是協程裏有一個很厲害的函數 withContext().

這個函數可以指定線程來執行代碼,並且在執行完成之後 自動把線程切回來 繼續執行。

launch(Dispatchers.Main) {
    val image = withContext(Dispatchers.IO){
        getImage(imageId)
    }
    iv.setImageBitmap(image)
}

這種寫法跟剛纔看起來區別不大。 但是如果有了更多的線程切換區別就體現出來了,由於有了自動切回來的功能,協程消除了併發代碼在協作時的嵌套,直接寫成了上下關係的代碼就能讓多線程之間進行協作,這就是協程。

以上就是本節內容,歡迎大家關注👇👇👇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章