協程是輕量級的線程。
kotlin協程是kotlin的擴展庫(kotlinx.coroutines)。
線程在Android開發中一般用來做一些復雜耗時的操作,避免耗時操作阻塞主線程而出現ANR的情況,例如IO操作就需要在新的線程中去完成。如果一個頁面中使用的線程太多,線程間的切換是很消耗內存資源的,我們都知道線程是由系統去控制調度的,所以線程使用起來比較難于控制。kotlin協程是運行在線程之上的,它的切換由程序自己來控制,無論是 CPU 的消耗還是內存的消耗都大大降低
它們在某些 CoroutineScope 上下文中與 launch協程構建器 一起啟動。
啟動協程的三種方式
1. runBlocking:T
2. launch:Job
3. async/await:Deferred
啟動模式
viewModelScope.launch(start = CoroutineStart.LAZY) {}
start 指定啟動模式
在 Kotlin 協程當中,啟動模式是一個枚舉:四個啟動模式當中我們最常用的其實是 DEFAULT 和 LAZY
CoroutineStart. LAZY只有在需要的情況下運行
LAZY 是懶漢式啟動,launch 后并不會有任何調度行為,協程體也自然不會進入執行狀態,直到我們需要它執行的時候
調用 Job.start,主動觸發協程的調度執行
調用 Job.join,隱式的觸發協程的調度執行
CoroutineStart. DEFAULT立即執行協程體
launch 調用后,會立即進入待調度狀態,一旦調度器 OK 就可以開始執行
CoroutineStart. ATOMIC立即執行協程體,但在開始運行之前無法取消.因此ATOMIC 模式,因此協程一定會被調度
CoroutineStart. UNDISPATCHED立即在當前線程執行協程體,直到第一個 suspend 調用
在GlobalScope 中啟動了一個新的協程,這意味著新協程的生命周期只受整個應用程序的生命周期限制。
我們可以在執行操作 所在的 指定作用域內啟動協程, 而不是像通常使用線程(線程總是全局的)那樣在 GlobalScope 中啟動。
包括 runBlocking 在內的每個協程構建器都將 CoroutineScope 的實例添加到其代碼塊所在的作用域中。
除了由不同的構建器提供協程作用域之外,還可以使用 coroutineScope 構建器聲明自己的作用域。它會創建一個協程作用域并且在所有已啟動子協程執行完畢之前不會結束。
將launch { ...... 內部的代碼塊提取到獨立的函數中。當你對這段代碼執行“提取函數”重構時,你會得到一個帶有suspend修飾符的新函數。
在協程內部可以像普通函數一樣使用掛起函數, 不過其額外特性是,同樣可以使用其他掛起函數(如本例中的 delay****)來掛起協程的執行。
launch 函數返回了一個 可以 被用來 取消運行中 的協程的 Job
job.cancel() 取消該作業
job.join() ****等待作業執行結束
一旦 main 函數調用了 job.cancel,我們在其它的協程中就看不到任何輸出,因為它被取消了。
這里也有一個可以使 Job 掛起的函數 cancelAndJoin 它合并了對 cancel 以及 join 的調用。
協程的取消是 協作 的。
一段協程代碼必須協作才能被取消。
所有 kotlinx.coroutines 中的掛起函數都是 可被取消的 。
它們檢查協程的取消, 并在取消時拋出 CancellationException。 然而,如果協程正在執行計算任務,并且沒有檢查取消的話,那么它是不能被取消的
。isActive 是一個可以被使用在 CoroutineScope 中的擴展屬性。
有兩種方法來使執行計算的代碼可以被取消。
第一種方法是定期調用掛起函數來檢查取消。對于這種目的 yield是一個好的選擇。
另一種方法是顯式的檢查取消狀態(isActive)
概念上,async 就類似于 launch。
它啟動了一個單獨的協程,這是一個輕量級的線程并與其它所有的協程一起并發的工作。
不同之處在于
launch返回一個 Job 并且不附帶任何結果值,
async返回一個 Deferred---一個輕量級的非阻塞 future, 這代表了一個將會在稍后提供結果的 promise。
你可以使用 .await()在一個延期的值上得到它的最終結果,
但是 Deferred也是一個 Job,所以如果需要的話,你可以取消它。
async 可以通過將 start參數設置為 CoroutineStart.LAZY 而變為惰性的。 在這個模式下,只有結果通過await 獲取的時候協程才會啟動,或者在 Job 的 start 函數調用的時候
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }// 執行一些計算
one.start() // 啟動第一個
two.start() // 啟動第二個
println("The answer is ${one.await() + two.await()}")
請注意,如果其中一個子協程(即 two)失敗,第一個 async以及等待中的父協程都會被取消
協程調度器可以將協程限制在一個特定的線程執行,或將它分派到一個線程池,亦或是讓它不受限地運行
所有的協程構建器諸如 launch 和 async 接收一個可選的 CoroutineContext參數,它可以被用來顯式的為一個新協程或其它上下文元素指定一個調度器。
當調用 launch { ...... } 時不傳參數,它從啟動了它的 CoroutineScope 中承襲了上下文(以及調度器)
當協程在 GlobalScope 中啟動時,使用的是由 Dispatchers.Default 代表的默認調度器。
默認調度器使用共享的后臺線程池。
所以 launch(Dispatchers.Default) { ...... }與 GlobalScope.launch { ...... }使用相同的調度器
使用 runBlocking 來顯式指定了一個上下文,并且另一個使用 withContext 函數來改變協程的上下文,而仍然駐留在相同的協程中
當一個協程 被 其它協程 在 CoroutineScope 中啟動的時候,
它將通過 CoroutineScope.coroutineContext 來承襲上下文,
并且這個新協程的 Job 將會成為父協程作業的 子 作業。
當一個父協程被取消的時候,所有它的子協程也會被遞歸的取消。
我們通過創建一個 CoroutineScope 實例來管理協程的生命周期,并使它與 activity 的生命周期相關聯。CoroutineScope****可以通過 CoroutineScope() 創建或者通過MainScope() 工廠函數。前者創建了一個通用作用域,而后者為使用 Dispatchers.Main 作為默認調度器的 UI 應用程序
classActivity{
private val mainScope = MainScope()
fun destroy() { mainScope.cancel() }
如果使用一些消耗 CPU 資源的阻塞代碼計算數字(每次計算需要 100 毫秒)那么我們可以使用 Sequence 來表示數字
suspend fun simple(): List<Int> {
delay(1000) // pretend we are doing something asynchronous here
return listOf(1, 2, 3)
}
fun main() = runBlocking<Unit> {
simple().forEach { value -> println(value) }
}
使用 List 結果類型,意味著我們只能一次返回所有值。
為了表示異步計算的值流(stream),我們可以使用 Flow 類型(正如同步計算值會使用 Sequence 類型)
fun simple(): **Flow<Int>** = **flow { // flow builder**
for (i in 1..3) {
delay(100) // pretend we are doing something useful here
**emit(i) // emit next value**
}
}
fun main() = runBlocking<Unit> {
launch {
for (k in 1..3) {
println("I'm not blocked $k")
delay(100)
}
**simple().collect { value -> println(value) }**
}}
注意使用 Flow 的代碼與先前示例的下述區別:名為 flow 的 Flow 類型構建器函數。flow { ... }構建塊中的代碼可以掛起。所以函數 simple 不再標有 suspend 修飾符。
流使用 emit 函數 發射 值
流使用 collect 函數 收集 值
啟動協程需要三樣東西,分別是 上下文****、啟動模式、協程體,
協程體 就好比 Thread.run
當中的代碼
啟動協程 三種方式:
Launch 返回Job
async 返回deferred,延遲稍后才能拿到 await獲取
runblocking(僅僅用于單元測試)
調度器(上下文 CoroutineContext)
調度器的目的就是切線程
本身是協程上下文的子類,同時實現了攔截器的接口, dispatch 方法會在攔截器的方法 interceptContinuation 中調用,進而實現協程的調度
現成的,它們定義在 Dispatchers 當中
Dispatchers.main UI 線程
Dispatchers.io 它基于 Default 調度器背后的線程池,并實現了獨立的隊列和限制,因此協程調度器從 Default 切換到 IO 并不會觸發線程切換。
Dispatchers.default 線程池
Dispatchers.unconfined 直接執行
協程攔截器是一個上下文的實現方向,攔截器可以左右你的協程的執行,同時為了保證它的功能的正確性,協程上下文集合永遠將它放在最后面
它攔截協程的方法也很簡單,因為協程的本質就是回調 + “黑魔法”,而這個回調就是被攔截的 Continuation 了。調度器就是基于攔截器實現的,換句話說調度器就是攔截器的一種。
如果我們在攔截器當中自己處理了線程切換,那么就實現了自己的一個簡單的調度器
自定義一個攔截器
class MyContinuationInterceptor: ContinuationInterceptor{
override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>) = MyContinuation(continuation)
}
class MyContinuation<T>(val continuation: Continuation<T>): Continuation<T> {
override val context = continuation.context
override fun resumeWith(result: Result<T>) {
log("<MyContinuation> $result" )//打印日志
continuation.resumeWith(result)
}
}
調用自定義攔截器 放到我們的協程上下文
GlobalScope.launch(MyContinuationInterceptor()) {}
withContext****這個函數可以切換到指定的線程,并在閉包內的邏輯執行結束之后,自動把線程切回去繼續執行。
**withContext**
是一個 **suspend**
函數,它需要在協程或者是另一個 **suspend**
函數中調用
通過 withContext
源碼可以知道,它本身就是一個掛起函數,它接收一個 Dispatcher
參數,依賴這個 Dispatcher
參數的指示,你的協程被掛起,然后切到別的線程。
**suspend**
是 Kotlin 協程最核心的關鍵字,幾乎所有介紹 Kotlin 協程的文章和演講都會提到它。它的中文意思是「暫停」或者「可掛起」
緊接著在 suspend
函數執行完成之后,協程為我們做的最爽的事就來了:會自動幫我們把線程再切回來。
我們的協程原本是運行在主線程的,當代碼遇到 suspend 函數的時候,發生線程切換,根據 Dispatchers
切換到了 IO 線程;
當這個函數執行完畢后,線程又切了回來,「切回來」也就是協程會 post
一個 Runnable
,讓剩下的代碼繼續回到主線程去執行。
協程在執行到有 suspend 標記的函數的時候,會被 suspend 也就是被掛起,而所謂的被掛起,就是切個線程;
不過區別在于,掛起函數在執行完成之后,協程會重新切回它原先的線程。
再簡單來講,在 Kotlin 中所謂的掛起,就是一個稍后會被自動切回來的線程調度操作。
這個「切回來」的動作,在 Kotlin 里叫做 resume,恢復。
suspend****關鍵字只起到了標志這個函數是一個耗時操作,必須放在協程中執行的作用,而withContext方法則進行了線程的切換工作
什么時候需要自定義 suspend 函數
如果你的某個函數比較耗時,也就是要等的操作,那就把它寫成 suspend 函數。這就是原則。
耗時操作一般分為兩類:I/O 操作和 CPU 計算工作。比如文件的讀寫、網絡交互、圖片的模糊處理,都是耗時的,通通可以把它們寫進 suspend 函數里。
另外這個「耗時」還有一種特殊情況,就是這件事本身做起來并不慢,但它需要等待,比如 5 秒鐘之后再做這個操作。這種也是 suspend 函數的應用場景。
具體該怎么寫
給函數加上 suspend 關鍵字,然后在 withContext 把函數的內容包住就可以了。
提到用 withContext是因為它在掛起函數里功能最簡單直接:把線程自動切走和切回。
當然并不是只有 withContext 這一個函數來輔助我們實現自定義的 suspend 函數,比如還有一個掛起函數叫 delay,它的作用是等待一段時間后再繼續往下執行代碼。
用 launch 函數來創建協程,其實還有其他兩個函數也可以用來創建協程:
- runBlocking
- async
runBlocking 通常適用于單元測試的場景,而業務開發中不會用到這個函數,因為它是線程阻塞的。
接下來我們主要來對比 launch 與 async 這兩個函數。
· 相同點:它們都可以用來啟動一個協程,返回的都是 Coroutine
,我們這里不需要糾結具體是返回哪個類。
· 不同點:**async**
返回的 Coroutine
多實現了 Deferred
接口。
關于 Deferred
它的意思就是延遲,也就是結果稍后才能拿到。
我們調用 Deferred.await()
就可以得到結果。
啟動一個協程可以使用 **launch**
或者 **async**
函數,協程其實就是這兩個函數中閉包的代碼塊
**Dispatchers**
調度器,它可以將協程限制在一個特定的線程執行,或者將它分派到一個線程池,或者讓它不受限制地運行
常用的 Dispatchers ,有以下三種:
- Dispatchers.Main:Android 中的主線程
- Dispatchers.IO:針對磁盤和網絡 IO 進行了優化,適合 IO 密集型的任務,比如:讀寫文件,操作數據庫以及網絡請求
- Dispatchers.Default:適合 CPU 密集型的任務,比如計算
協程就是個線程框架,協程的掛起本質就是線程切出去再切回來
? 協程就是切線程; 掛起就是可以自動切回來的切線程;
? 掛起的非阻塞式指的是它能用看起來阻塞的代碼寫出非阻塞的操作,就這么簡單
Job.join() 保證主線程 等待協程執行完畢
協程的取消 跟線程的取消差不多
Job.cancel
Job.cancelandjoin
那么協程自動進行線程切換的原理是什么?
Yield 讓出CPU,放棄調度控制權,回到上一次Resume的地方
Resume 獲取調度控制權,繼續執行程序,到上一次Yield的地方
https://mp.weixin.qq.com/s/RgAC1Q1J6BxnrJF12WQQ1w
https://developer.android.google.cn/topic/libraries/architecture/coroutines#viewmodelscope
Kotlin 協程提供了一個可供您編寫異步代碼的 API。通過 Kotlin 協程,您可以定義 CoroutineScope
,以幫助您管理何時應運行協程。每個異步操作都在特定范圍內運行。
架構組件針對應用中的邏輯范圍以及與 LiveData
的互操作層為協程提供了一流的支持。
使用 **liveData**
構建器函數 調用 **suspend**
函數,并將結果作為 LiveData
對象傳送
loadUser()
是在其他位置聲明的暫停函數。使用 liveData
構建器函數異步調用 loadUser()
,然后使用**emit()**
發出結果
val user: LiveData<User> = liveData {
// loadUser is a suspend function.
val data = database.loadUser()
emit(data)
}