Why
- 簡化異步代碼的編寫。
- 執(zhí)行嚴(yán)格主線程安全確保你的代碼永遠(yuǎn)不會意外阻塞主線程,并增強(qiáng)了代碼的可讀性。
- 提升代碼安全性,不會發(fā)生內(nèi)存泄露。
- 協(xié)程間通信。
What
協(xié)程的概念在編程語言的早期就出現(xiàn)了,在1967年Simula第一次使用協(xié)程。
協(xié)程就像非常輕量級的線程。
線程是由系統(tǒng)調(diào)度的,線程切換或線程阻塞的開銷都比較大。而協(xié)程依賴于線程,但是協(xié)程掛起時(shí)不需要阻塞線程,幾乎是無代價(jià)的,協(xié)程是由開發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程,非常輕量級,一個(gè)線程中可以創(chuàng)建任意個(gè)協(xié)程。
舉個(gè)通俗易懂的栗子,不一定準(zhǔn)確。假如要從 地鐵A站 去 地鐵C站 看和一個(gè)妹子約會,但是當(dāng)?shù)竭_(dá) 地鐵B站 的時(shí)候,你想來想去應(yīng)該去給妹子買個(gè)精美的禮物,大概要1個(gè)小時(shí),而從 A站 到 C站 只有這一輛列車,只不過開的飛快,每10分鐘又回到 A站重新出發(fā),地鐵好比一條線程,你去買禮物回到B站好比一個(gè)任務(wù)。在同步阻塞的情況下,是你去買禮物這段時(shí)間,地鐵一直等你,直到你帶著禮物回來。在有協(xié)程的情況下,你去買禮物好比一段協(xié)程,地鐵把你在B站放下(掛起),地鐵繼續(xù)開,你買好禮物了就在B站等下趟地鐵來,繼續(xù)上車(恢復(fù))前去約妹子。在異步的情況下是,你去買禮物(異步任務(wù)),地鐵繼續(xù)往前開,但是地鐵司機(jī)給你一個(gè)電話號碼(callback),你買禮物回到B站的時(shí)候需要打我的電話號碼,才讓你上車。異步callback的時(shí)候有個(gè)問題,每個(gè)人下車去臨時(shí)辦事司機(jī)還要給他一個(gè)電話號碼,如果他出異常不回來了,可能會導(dǎo)致司機(jī)的電話號碼泄露,非常不安全。
How
在Android開發(fā)中,經(jīng)常遇到的問題:
- Long running tasks
- Main-safety
- Leak work
Long running tasks
- 一次CPU循環(huán)小于0.0000000004秒
- 一次網(wǎng)絡(luò)請求大約0.4秒
在Android中主線程主要用戶UI的渲染和響應(yīng)用戶手勢交互,以及輕量級的邏輯運(yùn)算。若果在主線程發(fā)起一個(gè)請求,將會導(dǎo)致應(yīng)用變慢、變卡、無法響應(yīng)用戶的交互,很容易造成ARN,用戶體驗(yàn)極差。所以業(yè)界通行的做法是通過callback實(shí)現(xiàn)異步回調(diào):
class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}
上面callback的示例只是一層回調(diào)的情況,假如有兩個(gè)甚至更多的異步請求,而且存在下一個(gè)請求依賴上一個(gè)請求的結(jié)果,就會存在層層嵌套,當(dāng)然目前比較流行的做法是用Retrofit的轉(zhuǎn)換函數(shù)flatMap實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,但是代碼看起來還是很臃腫。如果使用協(xié)程上面的代碼可以簡化成這樣:
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
suspend fun get(url: String) = withContext(Dispatchers.IO) {
// Make a request
// Dispatchers.IO
}
Coroutines提供一個(gè)很好途徑可以簡化耗時(shí)任務(wù)的代碼編寫,使得異步callback的代碼可以像同步代碼一樣順序編寫。Coroutines在普通的方法上面加上兩個(gè)新的操作。除了call 并 return,Coroutines還增加 suspend 和 resume。
協(xié)程使用棧幀管理當(dāng)前運(yùn)行的方法和方法的所有本地變量。當(dāng)協(xié)程開始掛起,當(dāng)前棧幀被復(fù)制并保存以供后續(xù)使用。當(dāng)協(xié)程開始被恢復(fù),棧幀將從它被保存的地方恢復(fù)回來,當(dāng)前棧幀的方法繼續(xù)執(zhí)行。
- suspend: 掛起當(dāng)前協(xié)程的執(zhí)行,將當(dāng)前執(zhí)行棧幀的所有本地變量和函數(shù)copy出來并保存。
- resume: 從掛起的地方繼續(xù)當(dāng)前協(xié)程的執(zhí)行。
suspend functions
只能在協(xié)程或者suspend functions
中被調(diào)用。
Main-safety with coroutines
在Kotlin協(xié)程中,寫的好的suspend functions
總是應(yīng)該可以安全的從主線程被調(diào)用,也應(yīng)該允許從任何線程被調(diào)用。使用suspend
修飾的 function
并不是告訴Kotlin這個(gè)方法在主線程運(yùn)行。
為了寫一個(gè)主線程安全的耗時(shí)方法,你可以讓協(xié)程在Default
或者 IO
dispatcher中執(zhí)行(用withContext(Dispatchers.IO)
指定在IO線程中運(yùn)行)。在協(xié)程所有的協(xié)程必須運(yùn)行在dispatcher中,即使他們運(yùn)行在主線程中。Coroutines將會掛起自己,dispatcher知道如何恢復(fù)他們。
為了指定coroutines在什么線程運(yùn)行,kotlin提供了四種Dispatchers:
Dispatchers | 用途 | 使用場景 |
---|---|---|
Dispatchers.Main | 主線程,和UI交互,執(zhí)行輕量任務(wù) | 1.call suspend functions 。2. call UI functions 。 3. Update LiveData
|
Dispatchers.IO | 用于網(wǎng)絡(luò)請求和文件訪問 | 1. Database 。 2.Reading/writing files 。3. Networking
|
Dispatchers.Default | CPU密集型任務(wù) | 1. Sorting a list 。 2.Parsing JSON 。 3.DiffUtils
|
Dispatchers.Unconfined | 不限制任何制定線程 | 高級調(diào)度器,不應(yīng)該在常規(guī)代碼里使用 |
假如你在Room中使用
suspend functions
、RxJava
、LiveData
,它自動提供了主線程安全。
// 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
Leak work
你的程序里面可能會有成千上萬個(gè)協(xié)程,你很難通過代碼手動追蹤它們,假如你通過代碼手動追蹤他們以確保它們完成或取消,那么代碼會顯得臃腫且很容易出錯。如果代碼不是很完美,可能會失去對coroutine的追蹤,并導(dǎo)致任務(wù)泄露。任務(wù)泄露就像內(nèi)存泄露,但是更嚴(yán)重。它不但會浪費(fèi)內(nèi)存的使用,還有cpu、磁盤,甚至?xí)l(fā)起一個(gè)網(wǎng)絡(luò)請求。
在android中,我們知道Activity和Fragment等都是有生命周期的,我們通常的模式是當(dāng)前頁面退出的時(shí)候,取消所有的異步任務(wù)。假如有一個(gè)異步的網(wǎng)絡(luò)請求,在當(dāng)前頁面銷毀的時(shí)候還在執(zhí)行,會導(dǎo)致哪些問題:
- 空指針異常。為請求結(jié)果回來之后去更新UI狀態(tài),而意外導(dǎo)致空指針異常。
- 浪費(fèi)內(nèi)存資源。
- 浪費(fèi)CPU資源。
為了避免協(xié)程泄露,kotlin引入 結(jié)構(gòu)化并發(fā) 。結(jié)構(gòu)化并發(fā)是語言特性和最佳實(shí)踐的組合,如果我們遵循最佳實(shí)踐,將幫助追蹤運(yùn)行在協(xié)程中的任務(wù)。在Android中結(jié)構(gòu)化并發(fā)可以幫我們做如下三件事:
- 取消任務(wù),當(dāng)協(xié)程不再需要的時(shí)候。
- 追蹤任務(wù),當(dāng)協(xié)程運(yùn)行的時(shí)候。
- 傳播錯誤信號,當(dāng)協(xié)程執(zhí)行失敗的時(shí)候。
解決方式:
-
CoroutineScope
取消任務(wù),其實(shí)是通過關(guān)聯(lián)的job取消任務(wù)。 -
任務(wù)追蹤,coroutines的結(jié)構(gòu)化并發(fā)通過
coroutineScope
和supervisorScope
保證suspend function
的所有任務(wù)完成才返回。 -
傳播錯誤信號,
coroutineScope
保證錯誤雙向傳遞,只要有一個(gè)子coroutine失敗或出現(xiàn)異常,異常往父域傳遞,并取消所有的子coroutines。而supervisorScope
實(shí)現(xiàn)單向錯誤傳遞,適用于作業(yè)監(jiān)控器。
CoroutineScope
在kotlin中所有的協(xié)程必須運(yùn)行在CoroutineScope中,scope幫你追蹤所有協(xié)程的狀態(tài),但是它不像Dispatcher,并不運(yùn)行你的協(xié)程。它可以取消所有在里面啟動的協(xié)程。啟動一個(gè)新協(xié)程:
scope.launch {
// This block starts a new coroutine
// "in" the scope.
//
// It can call suspend functions
fetchDocs()
}
創(chuàng)建CoroutineScope
的常見方式如下:
-
CoroutineScope(context: CoroutineContext)
,api方法,如:val scope = CoroutineScope(Dispatchers.Main + Job())
,或者如下:
class LifecycleCoroutineScope : CoroutineScope, Closeable {
private val job = JobSupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun close() {
job.cancel()
}
}
class SimpleRetrofitActivity : FragmentActivity() {
private val activityScope = LifecycleCoroutineScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_retrofit)
// some other code ...
}
override fun onDestroy() {
super.onDestroy()
activityScope.close()
}
// some other code ...
}
-
coroutineScope
:api方法,創(chuàng)建新一個(gè)子域,并管理域中的所有協(xié)程。注意這個(gè)方法只有在block中創(chuàng)建的所有子協(xié)程全部執(zhí)行完畢后,才會退出。 -
supervisorScope
:與coroutineScope
的區(qū)別是在子協(xié)程失敗時(shí),錯誤不會往上傳遞給父域,所以不會影響子協(xié)程。
創(chuàng)建協(xié)程的常見方式如下:
-
lauch
:協(xié)程構(gòu)建器,創(chuàng)建并啟動(也可以延時(shí)啟動)一個(gè)協(xié)程,返回一個(gè)Job,用于監(jiān)督和取消任務(wù),用于無返回值的場景。 -
async
:協(xié)程構(gòu)建器,和launch一樣,區(qū)別是返回一個(gè)Job的子類Deferred
,唯一的區(qū)別是可以通過await獲取完成時(shí)的返回值,或者捕獲異常(異常處理也不一樣)。
在Android中有一個(gè)kotlin的ViewModel的擴(kuò)展庫 lifecycle-viewmodel-ktx:2.1.0-alpha04
,可以通過viewModelScope
擴(kuò)展屬性啟動協(xié)程,viewModelScope
綁定了activity的生命周期,activity銷毀的時(shí)候會自動取消在這個(gè)scope中啟動的所有協(xié)程。
fun runForever() {
// start a new coroutine in the ViewModel
viewModelScope.launch {
// cancelled when the ViewModel is cleared
while(true) {
delay(1_000)
// do something every second
}
}
}
注意,協(xié)程的取消是協(xié)作的,當(dāng)協(xié)程掛起的時(shí)候被取消將會拋一個(gè)
CancellationException
,即使你捕獲了這個(gè)異常,這個(gè)協(xié)程的狀態(tài)也變?yōu)槿∠麪顟B(tài)。假如你是一個(gè)計(jì)算協(xié)程,并且沒有檢查取消狀態(tài),那么這個(gè)協(xié)程不能被取消。
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // computation loop, just wastes CPU
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
任務(wù)追蹤
有時(shí),我們希望兩個(gè)或多個(gè)請求同時(shí)并發(fā),并等待他們?nèi)客瓿桑?code>suspend function 加上 coroutineScope
創(chuàng)建的子域可以保證全部子協(xié)程完成才返回。
suspend fun fetchTwoDocs() {
coroutineScope {
launch { fetchDoc(1) }
async { fetchDoc(2) }
}
}
傳播錯誤信號
注意協(xié)程的結(jié)構(gòu)化并發(fā)是基于語言特性加上最佳實(shí)踐的,如下方式會導(dǎo)致,錯誤丟失:
val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
// async without structured concurrency
unrelatedScope.async {
throw InAsyncNoOneCanHearYou("except")
}
}
上面代碼丟失錯誤是因?yàn)?async
的恢復(fù)需要調(diào)用await
,這樣才能將異常重新上傳,而在suspend function
使用了另外一個(gè)協(xié)程域,導(dǎo)致lostError
不會等待自作業(yè)的完成就退出了。正確的結(jié)構(gòu)化并發(fā):
suspend fun foundError() {
coroutineScope {
async {
throw StructuredConcurrencyWill("throw")
}
}
}
你可以通過
CoroutineScope
(注意是大寫開頭的C
) 和GlobalScope
來創(chuàng)建 非結(jié)構(gòu)化的協(xié)程,僅僅當(dāng)你認(rèn)為它的生命周期比調(diào)用者生命周期更長。
總結(jié)
-
CoroutineScope
:協(xié)程作用域包含CoroutineContext
,用于啟動協(xié)程,并追蹤子協(xié)程,其實(shí)是通過Job追蹤的。 -
CoroutineContext
:協(xié)程上下文,主要包含Job
和CoroutineDispatcher
,表示一個(gè)協(xié)程的場景。 -
CoroutineDispatcher
:協(xié)程調(diào)度器,決定協(xié)程所在的線程或線程池。它可以指定協(xié)程運(yùn)行于特定的一個(gè)線程、一個(gè)線程池或者不指定任何線程。 -
Job
:任務(wù),封裝了協(xié)程中需要執(zhí)行的代碼邏輯。Job 可以取消并且有簡單生命周期,它有三種狀態(tài):isActive
、isCompleted
、isCancelled
。 -
Deferred
:Job的子類,有返回值的Job,通過await
獲取。 - 協(xié)程構(gòu)建器包括:
lauch
、async
。