【譯】在 Android 上使用協程(一):Getting The Background

原文作者 :Sean McQuillan

原文地址: Coroutines on Android (part I): Getting the background

譯者 : 秉心說

這是關於在 Android 中使用協程的一系列文章。本篇讓我們先來看看協程是如何工作的以及它解決了什麼問題。

協程解決了什麼問題 ?

Kotlin 的 Coroutines (協程) 帶來了一種新的併發方式,在 Android 上,它可以用來簡化異步代碼。儘管 Kotlin 1.3 才帶來穩定版的協程,但是自編程語言誕生以來,協程的概念就已經出現了。第一個使用協程的語言是發佈於 1967 年的 Simula 。

在過去的幾年中,協程變得越來越流行。現在許多流行的編程語言都加入了協程,例如 Javascript , C# , Python , Ruby , Go 等等。Kotlin 協程基於以往構建大型應用中已建立的一些概念。

在安卓中,協程很好的解決了兩個問題:

  1. 耗時任務,運行時間過長阻塞主線程

  2. 主線程安全,允許你在主線程中調用任意 suspend(掛起) 函數

下面讓我們深入瞭解協程如何幫助我們構建更乾淨的代碼!

耗時任務

獲取網頁,和 API 進行交互,都涉及到了網絡請求。同樣的,從數據庫讀取數據,從硬盤中加載圖片,都涉及到了文件讀取。這些就是我們所說的耗時任務,App 不可能特地暫停下來等待它們執行完成。

和網絡請求相比,很難具體的想象現代智能手機執行代碼的速度有多快。Pixel 2 的一個 CPU 時鐘週期不超過 0.0000000004 秒,這是一個對人類來說很難理解的一個數字。但是如果你把一次網絡請求的耗時想象成一次眨眼,大概 0.4 s,這就很好理解 CPU 執行的到底有多快了。在一次眨眼的時間內,或者一次較慢的網絡請求,CPU 可以執行超過一百萬次時鐘週期。

在 Android 中,每個 app 都有一個主線程,負責處理 UI(例如 View 的繪製)和用戶交互。如果在主線程中處理過多任務,應用將會變得卡頓,隨之帶來了不好的用戶體驗。任何耗時任務都不應該阻塞主線程,

爲了避免在主線程中進行網絡請求,一種通用的模式是使用 CallBack(回調),它可以在將來的某一時間段回調進入你的代碼。使用回調訪問 developer.android.com 如下所示:

class ViewModelViewModel() {
   fun fetchDocs() {
       get("developer.android.com") { result ->
           show(result)
       }
    }
}

儘管 get() 方法是在主線程調用的,但它會在另一個線程中進行網絡請求。一旦網絡請求的結果可用了,回調就會在主線程中被調用。這是處理耗時任務的一種好方式,像 Retrofit 就可以幫助你進行網絡請求並且不阻塞主線程。

使用協程處理耗時任務

用協程來處理耗時任務可以簡化代碼。以上面的 fetchDocs() 方法爲例,我們使用協程來重寫之前的回調邏輯。

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.IO
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}
// look at this in the next p
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

上面的代碼會阻塞主線程嗎?它是如何在不暫停等待網絡請求或者阻塞主線程的情況下得到 get() 的返回值的?事實證明,Kotlin 協程提供了一種永遠不會阻塞主線程的代碼執行方式。

協程添加了兩個操作來構建一些常規功能。除了 invoke(or call) 和 return ,它額外添加了 suspend(掛起) 和 resume(恢復)

  • suspend —— 掛起當前協程的執行,保存所有局部變量

  • resume —— 從被掛起協程掛起的地方繼續執行

在 Kotlin 中,通過給函數添加 suspend 關鍵字來實現此功能。你只能在掛起函數中調用掛起函數,或者通過協程構造器,例如 launch ,來開啓一個新的協程。

掛起和恢復共同工作來替代回調。

在上面的例子中,get() 方法在進行網絡請求之前會掛起協程,它也負責進行網絡請求。然後,當網絡請求結束時,它僅僅只需要恢復之前掛起的協程,而不是調用回調函數來通知主線程。

這裏是一張生動形象的動圖,可是老是上傳失敗,可以點擊文末閱讀原文進行查看。

看一下 fetchDocs 是如何執行的,你就會明白 suspend 是如何工作的了。無論一個協程何時被掛起,它的當前棧幀(用來追蹤正在運行的函數及其變量)將被複制並保存。當進行 resume 時,棧幀將從之前被保存的地方複製回來並重新運行。在上面動畫的中間部分,當主線程上的所有協程都被掛起,就有時間去更新 UI,處理用戶事件。總之,掛起和恢復替代了回調,相當的整潔!

當主線程上的所有協程都被掛起,它就有時間做其他事情了。

即使我們直接順序書寫代碼,看起來就像是會導致阻塞的網絡請求一樣,但是協程會按我們所希望的那樣執行,不會阻塞主線程。

下面,讓我們看看協程是如何做到主線程安全的,並且探索一下 disaptchers(調度器)

協程的主線程安全

在 Kotlin 協程中,編寫良好的掛起函數在主線程中調用總是安全的。無論掛起函數做了什麼,總是應該允許任何線程調用它們。

但是,在 Android 應用中,我們如果把很多工作都放在主線程做會導致 APP 運行緩慢,例如網絡請求,JSON 解析,讀寫數據庫,甚至是大集合的遍歷。它們中任何一個都會導致應用卡頓,降低用戶體驗。所以它們不應該運行在主線程。

使用 suspend 並不意味着告訴 Kotlin 一定要在後臺線程運行函數。值得一提的是,協程經常運行在主線程。事實上,當啓動一個用於響應用戶事件的協程時,使用 Dispatchers.Main.immediate 是一個好主意。

協程也會運行在主線程,suspend 並不一定意味着後臺運行。

爲了讓一個函數不會使主線程變慢,我們可以告訴 Kotlin 協程使用 Default 或者 IO 調度器。在 Kotlin 中,所有的協程都需要使用調度器,即使它們運行在主線程。協程可以掛起自己,而調度器就是用來告訴它們如何恢復運行的。

爲了指定協程在哪裏運行,Kotlin 提供了 Dispatchers 來處理線程調度。

+-----------------------------------+
|         Dispatchers.Main          |
+-----------------------------------+
| Main thread on Android, interact  |
| with the UI and perform light     |
| work                              |
+-----------------------------------+
| - Calling suspend functions       |
| - Call UI functions               |
| - Updating LiveData               |
+-----------------------------------+

+-----------------------------------+
|          Dispatchers.IO           |
+-----------------------------------+
| Optimized for disk and network IO |
off the main thread               |
+-----------------------------------+
| - Database*                       |
| - Reading/writing files           |
| - Networking**                    |
+-----------------------------------+

+-----------------------------------+
|        Dispatchers.Default        |
+-----------------------------------+
| Optimized for CPU intensive work  |
off the main thread               |
+-----------------------------------+
| - Sorting a list                  |
| - Parsing JSON                    |
| - DiffUtils                       |
+-----------------------------------+
  • Room 在你使用 掛起函數 、RxJava 、LiveData 時自動提供主線程安全。

  • Retrofit 和 Volley 等網絡框架一般自己管理線程調度,當你使用 Kotlin 協程的時候不需要再顯式保證主線程安全。

繼續上面的例子,讓我們使用調度器來定義 get 函數。在 get 函數的方法體內使用 withContext(Dispatchers.IO)定義一段代碼塊,這個代碼塊將在調度器 Dispatchers.IO 中運行。方法塊中的任何代碼總是會運行在 IO 調度器中。由於 withContext 本身就是一個掛起函數,所以它通過協程提供了主線程安全。

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
    // Dispatchers.IO
    withContext(Dispatchers.IO) {
        // Dispatchers.IO
        /* perform blocking network IO here */
    }
    // Dispatchers.Main

通過協程,你可以細粒度的控制線程調度,因爲 withContext 讓你可以控制任意一行代碼運行在什麼線程上,而不用引入回調來獲取結果。你可將其應用在很小的函數中,例如數據庫操作和網絡請求。所以,比較好的做法是,使用 withContext 確保每個函數在任意調度器上執行都是安全的,包括 Main,這樣調用者在調用函數時就不需要考慮應該運行在什麼線程上。

編寫良好的掛起函數被任意線程調用都應該是安全的。

保證每個掛起函數主線程安全無疑是個好主意,如果它設計到任何磁盤,網絡,或者 CPU 密集型的任務,請使用 withContext 來確保主線程調用是安全的。這也是基於協程的庫所遵循的設計模式。如果你的整個代碼庫都遵循這一原則,你的代碼將會變得更加簡單,線程問題和程序邏輯也不會再混在一起。協程可以自由的從主線程啓動,數據庫和網絡請求的代碼會更簡單,且能保證用戶體驗。

withContext 的性能

對於提供主線程安全性,withContext 與回調或 RxJava 一樣快。在某些情況下,甚至可以使用協程上下文 withContext 來優化回調。如果一個函數將對數據庫進行10次調用,那麼您可以告訴 Kotlin 在外部的 withContext 中調用一次切換。儘管數據庫會重複調用 withContext ,但是他它將在同一個調度器下,尋找最快路徑。此外,Dispatchers.Default 和 Dispatchers.IO 之間的協程切換已經過優化,以儘可能避免線程切換。

What’s next

在這篇文章中我們探索了協程解決了什麼問題。協程是編程語言中一個非常古老的概念,由於它們能夠使與網絡交互的代碼更簡單,因此最近變得更加流行。

在安卓上,你可以使用協程解決兩個常見問題:

  • 簡化耗時任務的代碼,例如網絡請求,磁盤讀寫,甚至大量 JSON 的解析

  • 提供準確的主線程安全,在不會讓代碼更加臃腫的情況下保證不阻塞主線程

在下一篇文章中,我們將探索它們是如何適應 Android 的,以便跟蹤您從屏幕開始的所有工作!(In the next post we’ll explore how they fit in on Android to keep track of all the work you started from a screen! )

譯者說: 自我感覺翻譯的有點災難,不過災難也得翻譯下去,權當學習英語了!

文章首發微信公衆號: 秉心說 , 專注 Java 、 Android 原創知識分享,LeetCode 題解。

更多 JDK 源碼解析,掃碼關注我吧!

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