
8種機械鍵盤軸體對比
本人程式員,要買一個寫代碼的鍵盤,請問紅軸和茶軸怎麼選?
這是關于在Android上使用協程的一系列文章的一部分。這篇文章重點介紹了協程如何工作以及它們解決了哪些問題。
協程解決了哪些問題
Kotlin 協程引入了一種新的并發風格,可以在 Android 上用于簡化異步代碼。雖然它們是Kotlin 1.3 版中的新特性,但自程式設計語言誕生以來,協程的概念就已存在。Simula于1967年首次使用協程。
在過去幾年中,協程越來越受歡迎,現在已經包含在許多流行的程式設計語言中,例如 Javascript,C#,Python,Ruby 和 Go 等等。 Kotlin 協程基于已用于建構大型應用程序的既定概念。
在 Android 上,協程是解決兩個問題的絕佳解決方案:長時間運作的任務,是那些花費太長時間以至于阻塞主線程的任務
主安全性允許你確定可以從主線程調用所有挂起方法。
讓我們深入了解每一個,看看協程如何幫助我們以更整潔的方式建構代碼!
長時間運作的任務
擷取網頁或與API互動都涉及發出網絡請求。同樣,從資料庫讀取或從磁盤加載圖像涉及讀取文檔。這些東西就是我稱之為長時間運作的任務 - 任務花費太長時間讓你的應用程序要停下來等待它們!
與網絡請求相比,現代手機執行代碼的速度可能很難了解。在 Pixel 2 上,單個 CPU 周期僅需0.0000004秒,這個數字在對人來說可能沒什麼概念。但是,如果你将網絡請求視為一眨眼,大約400毫秒(0.4秒),則更容易了解 CPU 的運作速度。在一眨眼間,或者網絡請求有點慢,CPU可以執行超過一百萬次循環!
在 Android 上,每個應用程序都有一個主線程,負責處理UI(如繪制視圖)和協調使用者互動。如果此線程上發生了太多任務作,則應用程序似乎挂起或減速,進而導緻不良使用者體驗。任何長時間運作的任務都應該在不阻塞主線程的情況下完成,是以你的應用程序不會表現為所謂的“jank”,如卡頓的動畫,或者對觸摸事件響應緩慢。
為了從主線程執行網絡請求,常見的模式是回調。回調提供了一個庫的句柄,它可以用來在将來某個時候回調你的代碼。使用回調,抓取 developer.android.com 可能看起來像這樣:1
2
3
4
5
6
7class : ViewModel() {
fun () {
get("developer.android.com") { result ->
show(result)
}
}
}
即使從主線程調用 get ,它也将使用另一個線程來執行網絡請求。然後,一旦從網絡獲得結果,回調将在主線程上被調用。這是處理長時間運作任務的好方法,而像 Retrofit 這樣的庫可以幫助你在不阻塞主線程的情況下發出網絡請求。
使用協程執行長時間運作的任務
協程是一種簡化用于管理長期運作任務(如fetchDocs)的代碼的方法。為了探索協程如何使長時間運作的任務的代碼更簡單,讓我們重寫上面的回調示例以使用協同程序。1
2
3
4
5
6
7
8
9suspend fun fetchDocs() {
val result = get("developer.android.com")
show(result)
}
suspend fun get(url: String) = withContext(Dispatchers.IO){}
這段代碼不會阻塞主線程嗎?如何在不等待網絡請求和阻止的情況下從get傳回結果?事實證明,協程為 Kotlin 提供了一種執行此代碼的方法,并且永遠不會阻塞主線程。
協程通過添加兩個新操作來建構正常功能。除了 invoke 和 return 之外,協程還添加了 suspend 和 resume 。suspend - 暫停目前協同程序的執行,儲存所有局部變量
resume - 從暫停的地方繼續暫停協同程序
Kotlin 通過函數上的 suspend 關鍵字添加此功能。你隻能從其他挂起函數調用挂起函數,或者使用像 launch 這樣的協程 builder 來啟動新的協程。suspend 和 resume 通力合作取代了回調。
在上面的示例中,get将在啟動網絡請求之前挂起協程。函數get仍将負責從主線程運作網絡請求。然後,當網絡請求完成時,它可以簡單地恢複它挂起的協程,而不是調用回調來通知主線程。
展示kotlin如何實作 suspend 和 resume 來替換回調。
檢視 fetchDocs 的執行方式,你可以看到 suspend 的工作原理。每當協程挂起時,都會複制并儲存目前堆棧幀( Kotlin 用于跟蹤正在運作的函數及其變量的位置)以供日後使用。恢複後,堆棧幀将從儲存位置複制回來并再次開始運作。在動畫的中間 - 當主線程上的所有協程都被挂起時,主線程可以自由更新螢幕并處理使用者事件。suspend 和 resume 通力合作就能替換回調。很簡約!當主線程上的所有協程都被挂起時,主線程可以自由地做其他工作。
即使我們編寫了看起來完全像阻塞網絡請求的簡單順序代碼,協程也會按照我們想要的方式運作我們的代碼并避免阻塞主線程!
接下來,讓我們來看看如何使用協程來實作主線程安全并探索排程器(dispatchers)。
協程的主線程安全
在Kotlin協程中,編寫良好的挂起函數總是可以安全地從主線程調用。無論它們做什麼,它們都應該允許任何線程調用它們。
但是,我們在 Android 應用程序中做了很多事情,這些事情在主線程上發生得太慢了。網絡請求,解析JSON,從資料庫讀取或寫入,甚至隻是疊代大型清單。其中任何一個都有可能運作緩慢,導緻使用者可見的“jank”的任務應該在主線程之外運作。
使用 suspend 并不能告訴 Kotlin 在背景線程上運作函數。值得一提的是,并且通常協程将在主線程上運作。事實上,在響應UI事件時啟動協程時使用Dispatchers.Main.immediate 是一個非常好的主意 - 這樣,如果你沒有做需要主線程安全的長期運作任務,結果就可以在使用者的下一幀中可用。協程将在主線程上運作,suspend 并不意味着背景線程。
要編寫一個對主線程主安全來說太慢的函數,你可以告訴 Kotlin 協程在Default或IO排程程序上執行工作。在 Kotlin 中,所有協程必須在 dispatcher 中運作 - 即使它們在主線程上運作。協程可以自行挂起,dispatcher 知道如何恢複它們。
為了指定協程應該運作的位置,Kotlin 提供了三個可用于線程排程的排程器。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33+-----------------------------------+
| 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 将自動提供主線程安全性。與Kotlin協程一起使用時,Retrofit 和 Volley 等網絡庫管理自己的線程,并且在代碼中不需要明确的主線程安全性。
要繼續上面的示例,讓我們使用排程器來定義get函數。在你的函數體内部調用withContext(Dispatchers.IO) 來建立一個将在IO排程器上運作的塊。放在該塊中的任何代碼将始終在 IO 排程器上執行。由于 withContext 本身是一個 suspend 函數,是以它将使用協程來提供主線程的安全性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 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
}
// Dispatchers.Main
使用協程,你可以在很好的細粒度上控制進行線程排程。因為 withContext 允許你控制任何代碼行執行的線程而不引入回調以傳回結果,是以你·可以将它應用于非常小的函數,例如從資料庫讀取或執行網絡請求。是以一個好的做法是使用 withContext 來確定在包括Main在内的任何 Dispatcher 上調用每個函數都是安全的 - 這樣調用者就不必考慮執行該函數所需的線程。
在此示例中,fetchDocs正在主線程上執行,但可以安全地調用get,後者在背景執行網絡請求。因為協程支援挂起和恢複,是以隻要 withContext 塊完成,主線程上的協程就會恢複結果。
讓每個挂起功能都安全可靠是一個非常好的主意。如果它做任何觸及磁盤,網絡或甚至隻是使用太多 CPU 的東西,請使用withContext使其從主線程調用安全。這是基于coroutines的庫,如 Retrofit 和 Room 所遵循的。如果你在整個代碼庫中遵循此樣式,則代碼将更加簡單,并避免将線程問題與應用程序邏輯混合在一起。一緻地遵循協程,協程可以在主線程上自由啟動,并使用簡單的代碼發出網絡或資料庫請求,同時保證使用者不會看到“jank”。
withContext 的性能
在提供主線程安全的時候,withContext 與回調或 RxJava 一樣快。在某些情況下,可以在回調的範圍之外使用 withContext 進行優化。如果一個函數将對資料庫進行10次調用,則可以告訴 Kotlin 在所有10個調用的外部 withContext 中切換一次。然後,即使資料庫庫将重複調用 withContext ,它仍将保留在同一個排程器中并遵循快速路徑。此外,Dispatchers.Default 和 Dispatchers.IO 之間的切換已經過優化,以盡可能避免線程切換。
下一步是什麼
在這篇文章中,我們探讨了協程在解決問題方面遇到的問題。協程是程式設計語言中一個非常古老的概念,由于它們能夠使與網絡互動的代碼更簡單,是以最近變得流行。
在 Android 上,你可以使用它們來解決兩個非常常見的問題:簡化長時間運作任務的代碼,例如從網絡,磁盤讀取,甚至解析巨大的JSON結果。
執行精确的主線程安全性,以確定你不會在不使難以讀寫代碼的情況下意外阻塞主線程。