原文連接:Coroutines on Android (part I): Getting the backgroundhtml
原文做者:Sean McQuillanandroid
這是「怎樣在 Android 上使用協程」的系列文章的第一篇。git
這篇內容關注協程怎麼工做的以及它們解決什麼問題。github
Kotlin 的協程採用了一種新的併發方式(a new style of concurrency),能夠在 Android 上簡化異步代碼。數據庫
雖然在 Kotlin 1.3 協程做爲全新特性出現的,可是協程的概念從編程語言誕生之初就已經存在了。第一個探索使用協程的語言是的 Simula ,出如今 1967年。編程
最近幾年,協程愈來愈受歡迎,如今許多流行的編程語言裏都有協程,如 Javascript、C#、Python、Ruby等等。Kotlin 協程的設計基於它們這些構建過大型應用的經驗。安全
在 Android 上,協程能夠很是好的解決兩個問題:網絡
讓咱們深刻這兩個問題,看看協程是如何幫助咱們寫出更簡潔的代碼。併發
訪問網頁或和 API 交互都須要訪問網絡。一樣的,訪問數據庫或從硬盤加載圖片都須要讀取文件。這些就是我說的耗時任務——這些任務耗時太長,致使你的應用卡頓。異步
很難想象,現代手機執行代碼相比網絡請求有多快。在 Pixel 2上,一次 CPU 週期只須要 0.0000004 秒,這個數字從人類的角度上很難理解。然而,若是你把一次網絡請求當作一眨眼的時間,差很少 400 毫秒(0.4秒),這就比較好理解 CPU 執行有多快了。一次眨眼的時間,或者稍微慢一點的網絡請求中, CPU 能夠執行超過 100萬個週期。
在 Android 上,每一個應用程序都有一個主線程負責處理 UI (好比繪製視圖)和與用戶交互。若是在這個線程上作了太多工做,應用程序就會出現卡頓或者響應緩慢,從而致使很差的用戶體驗。任何耗時任務都不該該阻塞主線程,這樣你的應用就能避免例如觸摸反饋時響應緩慢的卡頓。
爲了在主線程之外執行網絡請求,一個常見的模式是 Callback,Callback 給了 library 一個 handle,它能夠用來在未來的某個時候調用你的代碼。使用 Callback 訪問 developer.android.com
看起來相似這樣:
class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}
複製代碼
即便在主線程調用 get
,它也會在另外一個線程執行網絡請求。而後,一旦從網絡中獲取到結果,就會在主線程上調用回調。這是處理耗時任務的好辦法,而經過 Retrofit 能夠幫助你在其餘線程發出網絡請求。
協程能夠簡化耗時任務,例如 fetchDocs
的代碼。爲了展現協程如何簡化耗時任務的代碼,讓咱們使用協程來重寫上面的 Callback 示例。
// 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
來啓動一個新的協程。
掛起配合上恢復代替回調
Suspend and resume work together to replace callbacks.
在上面的例子中,get
會在啓動網絡請求以前被 掛起 。而後get
函數會脫離主線程繼續負責運行網絡請求。而後,當網絡請求完成時,它不用回調來通知主線程,而是簡單的 恢復 掛起的協程。
查看fetchDocs
是如何執行的,你能夠看到 掛起 是如何工做的。當協程被掛起時,當前堆棧幀(Kotlin 用來跟蹤某個函數正在運行的位置及其變量)將會被複制並保存,用來之後使用。當它恢復,這個堆棧幀會被複制回來,並從新運行。在動畫的中間——當主線程上全部協程被掛起時,主線程能夠自由地更新屏幕並處理用戶事件。掛起配合恢復替換了回調,很是簡潔。
當主線程上的全部協程都掛起時,主線程能夠自由地執行其餘工做。
When all of the coroutines on the main thread are suspended, the main thread is free to do other work.
即便咱們編寫了與阻塞網絡請求徹底相同的直接順序的代碼,協程也將按照咱們但願的方式運行咱們的代碼,而且避免阻塞了主線程!
接下來,讓咱們看看如何使用協程實現主線程安全(main-safety),並探索調度流程。
在 Kotlin 協程中,編寫合適的掛起函數須要從主線程調用老是安全的。不管它們會作什麼,都應該始終容許任何線程能夠去調用它們。
可是,咱們在安卓應用中作的不少事情,對於主線程來講都太慢了。網絡請求、解析 JSON 、讀寫數據庫,甚至只是遍歷大型列表。其中任何一個都有可能由於太慢致使用戶能夠察覺到的延遲,因此應該脫離主線程運行。
使用 掛起 不是告訴 Kotlin 在一個後臺線程掛起。值的一提的說,協程一般在主線程運行。實際上,在響應一個 UI 事件的時候,使用 Dispatchers.Main.immediate 啓動一個協程是一個很是好的主意——這樣,若是你最終沒有在主線程執行耗時任務,那麼結果就會在下一幀提供給用戶。
協程將運行在主線程,而且掛起不表明在後臺
Coroutines will run on the main thread, and suspend does not mean background.
這樣的方式操做一個函數,會讓主線程變慢,你能夠告訴 Kotlin 協程在 Default 調度器或者 IO 調度器上執行工做。
在 Kotlin 中,全部的協程必須經過調度器運行,即便它們運行在主線程上。協程能夠 掛起 本身,而調度器知道怎麼 恢復 它們。
要指定協程應該運行在哪裏,Kotlin 提供了三個能夠用於切換線程的調度器。
+-----------------------------------+
| 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 |
+-----------------------------------+
複製代碼
* 若是你使用 掛起函數, RxJava, 或 LiveData,Room 會提供自動的主線程安全。
** 網絡庫(如 Retrofit 和 Volley)會管理它們本身的線程,當與 Kotlin 協程一塊兒使用時,不須要在代碼中顯式地聲明主線程安全。
繼續上面的示例,讓咱們使用調度器來定義 get
函數。在 get
的函數體中,咱們調用 withContext(Dispatchers.IO)
用來建立一個運行在 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
複製代碼
使用協程,你能夠對線程進行細粒度的劃分(With coroutines you can do thread dispatch with fine-grained control)。由於withContext
容許你控制在什麼線程上執行任何代碼塊,而不須要引入 callback 來返回結果,因此你能夠將它應用於很是小的函數,好比從數據庫讀取數據或執行網絡請求。所以,一個好的實踐是使用 withContext
來確保任何調度器(包括 Main)上調用每一個函數都是安全的——這樣調用者就沒必要考慮須要在哪一個線程執行函數。
在這個例子上,fetchDocs
在主線程上執行,可是能夠安全的調用get
函數,而後會在後臺執行網絡請求。由於協程支持掛起和恢復,因此只要 withContext
塊完成,主線程上的協程就會被恢復獲得結果。
寫的好的掛起函數從主線程調用老是安全的。
* Well written suspend functions are always safe to call from the main thread (or main-safe).
讓每一個掛起函數在主線程調用都是安全的是個好主意。若是它作了任何觸及磁盤、網絡甚至只是佔有太多 CPU 的操做,那麼就使用 withContext
來確保從主線程調用是安全。這是基於協程的庫(如 Retrofit 和 Room)所遵循的模式。若是你在整個代碼庫中都遵循這種風格,那麼你的代碼將會簡單的多,並避免將線程問題和應用程序邏輯混合在一塊兒。當遵循這個模式時,協程能夠在主線程上自由調用,用簡單的代碼請求網絡或數據庫,同時保證用戶不會看到卡頓。
對於提供主線程安全上,withContext
與使用回調或者 RxJava 同樣快。在某些狀況下,withContext
可能經過優化甚至比回調性能還好。若是一個函數將要訪問10次數據庫,你能夠告訴 Kotlin 使用 withContext
在 10 次的調用的外部切換一次(原文:If a function will make 10 calls to a database, you can tell Kotlin to switch once in an outer withContext
around all 10 calls. )。而後,即便數據庫會重複調用 withContext
,它也會保持在同一個調度器上,並遵循快速路徑。此外在 Default 調度器和 IO 調度器直接切換通過優化會盡量的避免線程切換。
在這篇文章中,咱們探討了協程最擅長解決的問題。協程在編程語言中是一個存在好久的概念,因爲它可以簡化與網絡交互的代碼,因此最近變得很是流行。
在 Android 上,你可使用它們解決兩個很是常見的問題:
在下一篇文章,咱們探索它們是如何配合 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! Give it a read:)。