簡介
協程是一種併發設計模式,可以使用協程來簡化異步代碼
爲什麼需要協程
順序執行的代碼是同步的,即下面的工作要執行必須等待之前的工作執行完畢,每一行代碼都會阻塞當前的線程,顯然主線程阻塞會導致明顯的卡頓 ,界面呈現速度緩慢或界面凍結,對觸摸事件的響應速度很慢,所以我們需要將耗時的任務放到主線程之外運行
多線程執行代碼,可以在不同的線程上執行順序的代碼,然後通過異步函數完成線程之間的切換工作,換言之異步回調即代碼的多線程順序執行
在Android平臺上,協程有助於解決兩個主要問題:
協程使用
首先在app/build.gradle中添加以下依賴庫
implementation
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
implementation
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
@Test
fun testCoroutine() {
GlobalScope.launch {// 創建一個協程的作用域
delay(1500L) // 非阻塞式掛起函數,會掛起當前協程,並不影響其他協程的運行
println("world!")
}
println("Hello, ")
Thread.sleep(2000L) // 會阻塞當前線程,運行在改下成寫的所有協程都會被阻塞,阻塞主線程保證JVM存活
}
結果:
Hello,
world!
當主線程的阻塞時間小於1500L時,只能打印出Hello,此時協程未來得及執行
爲了解決上述協程未來得及執行的問題:引入runBlocking
fun testCoroutine() {
runBlocking {// 創建一個協程的作用域,此時該作用於中的協程全部執行完之前一直阻塞當前線程(會影響主線程)
delay(1500L) // 非阻塞式掛起函數,會掛起當前協程,並不影響其他協程的運行
println("world!")
}
println("Hello, ")
Thread.sleep(1000L) // 會阻塞當前線程,運行在改下成寫的所有協程都會被阻塞,阻塞主線程保證JVM存活
}
// 結果:
world!
Hello,
1.使用launch函數啓動攜程
fun testCoroutine() {
runBlocking {
launch {
println("launch1")
delay(1000)
println("launch1 finish")
}
launch {
println("launch2")
delay(1000)
println("launch2 finish")
}
}
}
// 結果:
launch1
launch2
launch1 finish
launch2 finish
2. 使用async啓動協程
//三次請求併發進行
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
val deferredThree = async { fetchDoc(3) }
//所有結果全部返回後更新UI
updateUI(deferredOne.await(), deferredTwo.await(), deferredThree.await())
}
suspend fun fetchDoc(){...}
掛起
掛起的協程不會阻塞所在的線程但會阻塞當前掛起的協程,線程繼續執行下面的代碼,協程則等待直到線程回來執行協程中的代碼
協程的掛起即協程中的代碼離開協程所在的線程,協程恢復即協程中的代碼重新進入協程所在的線程,協程通過掛起恢復機制實現線程的切換
suspend關鍵字
suspend意爲掛起, 使用suspend聲名的函數和上面的delay函數一樣是掛起函數(掛起函數必須在協程或者其他掛起函數中被調用即掛起函數必須直接或者間接的在協程作用域中執行)
suspend函數並不能起到切換線程的作用,它的存在只是說明被它修飾的函數是一個耗時函數
suspend fun testSuspend(){
println("suspend ${Thread.currentThread().name}")
}
fun testCoroutine() {
GlobalScope.launch(Dispatchers.Main) {// 創建一個協程的作用域
println("Start ${Thread.currentThread().name}")
testSuspend()
println("End ${Thread.currentThread().name}")
}
}
// 結果:
Start main -> suspend main -> End main
使用suspend並不能切換線程,那麼我們就要使用另一個作用域構造器withContext
withContext使用說明
調用withContext之後,會執行代碼塊中的代碼同時會阻塞當前協程,當代碼塊執行完畢之後回京最後一行執行結果作爲withContext的返回值返回
withContext會要求傳入一個線程參數,因爲Android中I/O操作一般在子線程中進行,如果開啓主線程上的協程去做網絡請求顯然也是不可以的,所以必須要給協程指定一個線程去執行相應的操作
線程參數如下
Dispatchers.Main : 使用此調度程序可在Android主線程上運行協程,此調度程序只能用於與界面交互以及執行快速工作,這個值只能在Android項目中使用,純Kotlin程序使用此類型會報錯
Dispatchers.IO :此調度程序適合在主線程之外執行磁盤和網絡I/O,是一種叫高併發的線程策略
Dispatchers.Defaul t:此調度程序適合在主線程之外執行佔用大量的CPU資源的工作,是一種低併發的線程策略
使用withContext進行掛起函數中的線程切換
fun main() {
GlobalScope.launch(Dispatchers.Main) {// 創建一個協程的作用域
println("Start ${Thread.currentThread().name}")
testSuspend()
println("End ${Thread.currentThread().name}")
}
}
suspend fun testSuspend() {
withContext(Dispatchers.IO) {
println("suspend ${Thread.currentThread().name}")
}
}
// 結果
Hello main -> suspend DefaultDispatcher-worker-1 -> End main