原文做者 :Sean McQuillanjavascript
原文地址: Coroutines on Android (part I): Getting the backgroundhtml
譯者 : 秉心說java
這是關於在 Android 中使用協程的一系列文章。本篇讓咱們先來看看協程是如何工做的以及它解決了什麼問題。python
Kotlin 的 Coroutines (協程) 帶來了一種新的併發方式,在 Android 上,它能夠用來簡化異步代碼。儘管 Kotlin 1.3 才帶來穩定版的協程,可是自編程語言誕生以來,協程的概念就已經出現了。第一個使用協程的語言是發佈於 1967 年的 Simula 。android
在過去的幾年中,協程變得愈來愈流行。如今許多流行的編程語言都加入了協程,例如 Javascript , C# , Python , Ruby , Go 等等。Kotlin 協程基於以往構建大型應用中已創建的一些概念。git
在安卓中,協程很好的解決了兩個問題:github
下面讓咱們深刻了解協程如何幫助咱們構建更乾淨的代碼!golang
獲取網頁,和 API 進行交互,都涉及到了網絡請求。一樣的,從數據庫讀取數據,從硬盤中加載圖片,都涉及到了文件讀取。這些就是咱們所說的耗時任務,App 不可能特意暫停下來等待它們執行完成。數據庫
和網絡請求相比,很難具體的想象現代智能手機執行代碼的速度有多快。Pixel 2
的一個 CPU 時鐘週期不超過 0.0000000004
秒,這是一個對人類來講很難理解的一個數字。可是若是你把一次網絡請求的耗時想象成一次眨眼,大概 0.4 s,這就很好理解 CPU 執行的到底有多快了。在一次眨眼的時間內,或者一次較慢的網絡請求,CPU 能夠執行超過一百萬次時鐘週期。編程
在 Android 中,每一個 app 都有一個主線程,負責處理 UI(例如 View 的繪製)和用戶交互。若是在主線程中處理過多任務,應用將會變得卡頓,隨之帶來了很差的用戶體驗。任何耗時任務都不該該阻塞主線程,
爲了不在主線程中進行網絡請求,一種通用的模式是使用 CallBack
(回調),它能夠在未來的某一時間段回調進入你的代碼。使用回調訪問 developer.android.com
以下所示:
class ViewModel: ViewModel() {
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 section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}
複製代碼
上面的代碼會阻塞主線程嗎?它是如何在不暫停等待網絡請求或者阻塞主線程的狀況下獲得 get()
的返回值的?事實證實,Kotlin 協程提供了一種永遠不會阻塞主線程的代碼執行方式。
協程添加了兩個操做來構建一些常規功能。除了 invoke(or call)
和 return
,它額外添加了 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 |
+-----------------------------------+
複製代碼
繼續上面的例子,讓咱們使用調度器來定義 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 與回調或 RxJava 同樣快。在某些狀況下,甚至可使用協程上下文 withContext 來優化回調。若是一個函數將對數據庫進行10次調用,那麼您能夠告訴 Kotlin 在外部的 withContext 中調用一次切換。儘管數據庫會重複調用 withContext ,可是他它將在同一個調度器下,尋找最快路徑。此外,Dispatchers.Default
和 Dispatchers.IO
之間的協程切換已通過優化,以儘量避免線程切換。
在這篇文章中咱們探索了協程解決了什麼問題。協程是編程語言中一個很是古老的概念,因爲它們可以使與網絡交互的代碼更簡單,所以最近變得更加流行。
在安卓上,你可使用協程解決兩個常見問題:
在下一篇文章中,咱們將探索它們是如何適應 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! )
下一篇:在 Android 上使用協程(二):Getting started
譯者說: 自我感受翻譯的有點災難,不過災難也得翻譯下去,權當學習英語了!
文章首發微信公衆號:
秉心說
, 專一 Java 、 Android 原創知識分享,LeetCode 題解。更多 JDK 源碼解析,掃碼關注我吧!