新聞中心
接上篇文章《在 Android 開發(fā)中使用協(xié)程 | 背景介紹》

創(chuàng)新互聯(lián)是一家集網(wǎng)站建設(shè),泉港企業(yè)網(wǎng)站建設(shè),泉港品牌網(wǎng)站建設(shè),網(wǎng)站定制,泉港網(wǎng)站建設(shè)報價,網(wǎng)絡(luò)營銷,網(wǎng)絡(luò)優(yōu)化,泉港網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強企業(yè)競爭力。可充分滿足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實用型網(wǎng)站。
本文是介紹 Android 協(xié)程系列中的第二部分,這篇文章主要會介紹如何使用協(xié)程來處理任務(wù),并且能在任務(wù)開始執(zhí)行后保持對它的追蹤。
保持對協(xié)程的追蹤
本系列文章的第一篇,我們探討了協(xié)程適合用來解決哪些問題。這里再簡單回顧一下,協(xié)程適合解決以下兩個常見的編程問題:
- 處理耗時任務(wù) (Long running tasks),這種任務(wù)常常會阻塞住主線程;
- 保證主線程安全 (Main-safety),即確保安全地從主線程調(diào)用任何 suspend 函數(shù)。
協(xié)程通過在常規(guī)函數(shù)之上增加 suspend 和 resume 兩個操作來解決上述問題。當(dāng)某個特定的線程上的所有協(xié)程被 suspend 后,該線程便可騰出資源去處理其他任務(wù)。
協(xié)程自身并不能夠追蹤正在處理的任務(wù),但是有成百上千個協(xié)程并對它們同時執(zhí)行掛起操作并沒有太大問題。協(xié)程是輕量級的,但處理的任務(wù)卻不一定是輕量的,比如讀取文件或者發(fā)送網(wǎng)絡(luò)請求。
使用代碼來手動追蹤上千個協(xié)程是非常困難的,您可以嘗試對所有協(xié)程進行跟蹤,手動確保它們都完成了或者都被取消了,那么代碼會臃腫且易出錯。如果代碼不是很完美,就會失去對協(xié)程的追蹤,也就是所謂 "work leak" 的情況。
任務(wù)泄漏 (work leak) 是指某個協(xié)程丟失無法追蹤,它類似于內(nèi)存泄漏,但比它更加糟糕,這樣丟失的協(xié)程可以恢復(fù)自己,從而占用內(nèi)存、CPU、磁盤資源,甚至?xí)l(fā)起一個網(wǎng)絡(luò)請求,而這也意味著它所占用的這些資源都無法得到重用。
泄漏協(xié)程會浪費內(nèi)存、CPU、磁盤資源,甚至發(fā)送一個無用的網(wǎng)絡(luò)請求。
為了能夠避免協(xié)程泄漏,Kotlin 引入了結(jié)構(gòu)化并發(fā) (structured concurrency) 機制,它是一系列編程語言特性和實踐指南的結(jié)合,遵循它能幫助您追蹤到所有運行于協(xié)程中的任務(wù)。
在 Android 平臺上,我們可以使用結(jié)構(gòu)化并發(fā)來做到以下三件事:
- 取消任務(wù) —— 當(dāng)某項任務(wù)不再需要時取消它;
- 追蹤任務(wù) —— 當(dāng)任務(wù)正在執(zhí)行時,追蹤它;
- 發(fā)出錯誤信號 —— 當(dāng)協(xié)程失敗時,發(fā)出錯誤信號表明有錯誤發(fā)生。
接下來我們對以上幾點一一進行探討,看看結(jié)構(gòu)化并發(fā)是如何幫助能夠追蹤所有協(xié)程,而不會導(dǎo)致泄漏出現(xiàn)的。
結(jié)構(gòu)化并發(fā):
https://kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency
借助 scope 來取消任務(wù)
在 Kotlin 中,定義協(xié)程必須指定其 CoroutineScope 。CoroutineScope 可以對協(xié)程進行追蹤,即使協(xié)程被掛起也是如此。同第一篇文章中講到的調(diào)度程序 (Dispatcher) 不同,CoroutineScope 并不運行協(xié)程,它只是確保您不會失去對協(xié)程的追蹤。
為了確保所有的協(xié)程都會被追蹤,Kotlin 不允許在沒有使用 CoroutineScope 的情況下啟動新的協(xié)程。CoroutineScope 可被看作是一個具有超能力的 ExecutorService 的輕量級版本。它能啟動新的協(xié)程,同時這個協(xié)程還具備我們在第一部分所說的 suspend 和 resume 的優(yōu)勢。
CoroutineScope 會跟蹤所有協(xié)程,同樣它還可以取消由它所啟動的所有協(xié)程。這在 Android 開發(fā)中非常有用,比如它能夠在用戶離開界面時停止執(zhí)行協(xié)程。
CoroutineScope 會跟蹤所有協(xié)程,并且可以取消由它所啟動的所有協(xié)程。
啟動新的協(xié)程
需要特別注意的是,您不能隨便就在某個地方調(diào)用 suspend 函數(shù),suspend 和 resume 機制要求您從常規(guī)函數(shù)中切換到協(xié)程。
有兩種方式能夠啟動協(xié)程,它們分別適用于不同的場景:
- launch 構(gòu)建器適合執(zhí)行 "一勞永逸" 的工作,意思就是說它可以啟動新協(xié)程而不將結(jié)果返回給調(diào)用方;
- async 構(gòu)建器可啟動新協(xié)程并允許您使用一個名為 await 的掛起函數(shù)返回 result。
通常,您應(yīng)使用 launch 從常規(guī)函數(shù)中啟動新協(xié)程。因為常規(guī)函數(shù)無法調(diào)用 await (記住,它無法直接調(diào)用 suspend 函數(shù)),所以將 async 作為協(xié)程的主要啟動方法沒有多大意義。稍后我們會討論應(yīng)該如何使用 async。
您應(yīng)該改為使用 coroutine scope 調(diào)用 launch 方法來啟動協(xié)程。
- scope.launch {
- // 這段代碼在作用域里啟動了一個新協(xié)程
- // 它可以調(diào)用掛起函數(shù)
- fetchDocs()
- }
您可以將 launch 看作是將代碼從常規(guī)函數(shù)送往協(xié)程世界的橋梁。在 launch 函數(shù)體內(nèi),您可以調(diào)用 suspend 函數(shù)并能夠像我們上一篇介紹的那樣保證主線程安全。
Launch 是將代碼從常規(guī)函數(shù)送往協(xié)程世界的橋梁。
注意:launch 和 async 之間的很大差異是它們對異常的處理方式不同。async 期望最終是通過調(diào)用 await 來獲取結(jié)果 (或者異常),所以默認情況下它不會拋出異常。這意味著如果使用 async 啟動新的協(xié)程,它會靜默地將異常丟棄。
由于 launch 和 async 僅能夠在 CouroutineScope 中使用,所以任何您所創(chuàng)建的協(xié)程都會被該 scope 追蹤。Kotlin 禁止您創(chuàng)建不能夠被追蹤的協(xié)程,從而避免協(xié)程泄漏。
- launchhttps://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
- asynchttps://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
在 ViewModel 中啟動協(xié)程
既然 CoroutineScope 會追蹤由它啟動的所有協(xié)程,而 launch 會創(chuàng)建一個新的協(xié)程,那么您應(yīng)該在什么地方調(diào)用 launch 并將其放在 scope 中呢? 又該在什么時候取消在 scope 中啟動的所有協(xié)程呢?
在 Android 平臺上,您可以將 CoroutineScope 實現(xiàn)與用戶界面相關(guān)聯(lián)。這樣可讓您避免泄漏內(nèi)存或者對不再與用戶相關(guān)的 Activities 或 Fragments 執(zhí)行額外的工作。當(dāng)用戶通過導(dǎo)航離開某界面時,與該界面相關(guān)的 CoroutineScope 可以取消掉所有不需要的任務(wù)。
結(jié)構(gòu)化并發(fā)能夠保證當(dāng)某個作用域被取消后,它內(nèi)部所創(chuàng)建的所有協(xié)程也都被取消。
當(dāng)將協(xié)程同 Android 架構(gòu)組件 (Android Architecture Components) 集成起來時,您往往會需要在 ViewModel 中啟動協(xié)程。因為大部分的任務(wù)都是在這里開始進行處理的,所以在這個地方啟動是一個很合理的做法,您也不用擔(dān)心旋轉(zhuǎn)屏幕方向會終止您所創(chuàng)建的協(xié)程。
從生命周期感知型組件 (AndroidX Lifecycle) 的 2.1.0 版本開始 (發(fā)布于 2019 年 9 月),我們通過添加擴展屬性 ViewModel.viewModelScope 在 ViewModel 中加入了協(xié)程的支持。
看看如下示例:
- class MyViewModel(): ViewModel() {
- fun userNeedsDocs() {
- // 在 ViewModel 中啟動新的協(xié)程
- viewModelScope.launch {
- fetchDocs()
- }
- }
- }
當(dāng) viewModelScope 被清除 (當(dāng) onCleared() 回調(diào)被調(diào)用時) 之后,它將自動取消它所啟動的所有協(xié)程。這是一個標(biāo)準(zhǔn)做法,如果一個用戶在尚未獲取到數(shù)據(jù)時就關(guān)閉了應(yīng)用,這時讓請求繼續(xù)完成就純粹是在浪費電量。
為了提高安全性,CoroutineScope 會進行自行傳播。也就是說,如果某個協(xié)程啟動了另一個新的協(xié)程,它們都會在同一個 scope 中終止運行。這意味著,即使當(dāng)某個您所依賴的代碼庫從您創(chuàng)建的 viewModelScope 中啟動某個協(xié)程,您也有方法將其取消。
注意:協(xié)程被掛起時,系統(tǒng)會以拋出 CancellationException 的方式協(xié)作取消協(xié)程。捕獲頂級異常 (如Throwable) 的異常處理程序?qū)⒉东@此異常。如果您做異常處理時消費了這個異常,或從未進行 suspend 操作,那么協(xié)程將會徘徊于半取消 (semi-canceled) 狀態(tài)下。
所以,當(dāng)您需要將一個協(xié)程同 ViewModel 的生命周期保持一致時,使用 viewModelScope 來從常規(guī)函數(shù)切換到協(xié)程中。然后,viewModelScope 會自動為您取消協(xié)程,因此在這里哪怕是寫了死循環(huán)也是完全不會產(chǎn)生泄漏。如下示例:
- fun runForever() {
- // 在 ViewModel 中啟動新的協(xié)程
- viewModelScope.launch {
- // 當(dāng) ViewModel 被清除后,下列代碼也會被取消
- while(true) {
- delay(1_000)
- // 每過 1 秒做點什么
- }
- }
- }
通過使用 viewModelScope,可以確保所有的任務(wù),包含死循環(huán)在內(nèi),都可以在不需要的時候被取消掉。
協(xié)作取消:
https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#cancellation-and-timeouts
任務(wù)追蹤
使用協(xié)程來處理任務(wù)對于很多代碼來說真的很方便。啟動協(xié)程,進行網(wǎng)絡(luò)請求,將結(jié)果寫入數(shù)據(jù)庫,一切都很自然流暢。
但有時候,可能會遇到稍微復(fù)雜點的問題,例如您需要在一個協(xié)程中同時處理兩個網(wǎng)絡(luò)請求,這種情況下需要啟動更多協(xié)程。
想要創(chuàng)建多個協(xié)程,可以在 suspend function 中使用名為 coroutineScope 或 supervisorScope 這樣的構(gòu)造器來啟動多個協(xié)程。但是這個 API 說實話,有點令人困惑。coroutineScope 構(gòu)造器和 CoroutineScope 這兩個的區(qū)別只是一個字符之差,但它們卻是完全不同的東西。
另外,如果隨意啟動新協(xié)程,可能會導(dǎo)致潛在的任務(wù)泄漏 (work leak)。調(diào)用方可能感知不到啟用了新的協(xié)程,也就意味著無法對其進行追蹤。
為了解決這個問題,結(jié)構(gòu)化并發(fā)發(fā)揮了作用,它保證了當(dāng) suspend 函數(shù)返回時,就意味著它所處理的任務(wù)也都已完成。
結(jié)構(gòu)化并發(fā)保證了當(dāng) suspend 函數(shù)返回時,它所處理任務(wù)也都已完成。
示例使用 coroutineScope 來獲取兩個文檔內(nèi)容:
- suspend fun fetchTwoDocs() {
- coroutineScope {
- launch { fetchDoc(1) }
- async { fetchDoc(2) }
- }
- }
在這個示例中,同時從網(wǎng)絡(luò)中獲取兩個文檔數(shù)據(jù),第一個是通過 launch 這樣 "一勞永逸" 的方式啟動協(xié)程,這意味著它不會返回任何結(jié)果給調(diào)用方。
第二個是通過 async 的方式獲取文檔,所以是會有返回值返回的。不過上面示例有一點奇怪,因為通常來講兩個文檔的獲取都應(yīng)該使用 async,但這里我僅僅是想舉例來說明可以根據(jù)需要來選擇使用 launch 還是 async,或者是對兩者進行混用。
coroutineScope 和 supervisorScope 可以讓您安全地從 suspend 函數(shù)中啟動協(xié)程。
但是請注意,這段代碼不會顯式地等待所創(chuàng)建的兩個協(xié)程完成任務(wù)后才返回,當(dāng) fetchTwoDocs 返回時,協(xié)程還正在運行中。
所以,為了做到結(jié)構(gòu)化并發(fā)并避免泄漏的情況發(fā)生,我們想做到在諸如 fetchTwoDocs 這樣的 suspend 函數(shù)返回時,它們所做的所有任務(wù)也都能結(jié)束。換個說法就是,fetchTwoDocs 返回之前,它所啟動的所有協(xié)程也都能完成任務(wù)。
Kotlin 確保使用 coroutineScope 構(gòu)造器不會讓 fetchTwoDocs 發(fā)生泄漏,coroutinScope 會先將自身掛起,等待它內(nèi)部啟動的所有協(xié)程完成,然后再返回。因此,只有在 coroutineScope 構(gòu)建器中啟動的所有協(xié)程完成任務(wù)之后,fetchTwoDocs 函數(shù)才會返回。
- coroutineScope:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
- supervisorScope:https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
處理一堆任務(wù)
既然我們已經(jīng)做到了追蹤一兩個協(xié)程,那么來個刺激的,追蹤一千個協(xié)程來試試!
先看看下面這個動畫
這個動畫展示了 coroutineScope 是如何追蹤一千個協(xié)程的
這個動畫向我們展示了如何同時發(fā)出一千個網(wǎng)絡(luò)請求。當(dāng)然,在真實的 Android 開發(fā)中最好別這么做,太浪費資源了。
這段代碼中,我們在 coroutineScope 構(gòu)造器中使用 launch 啟動了一千個協(xié)程,您可以看到這一切是如何聯(lián)系到一起的。由于我們使用的是 suspend 函數(shù),因此代碼一定使用了 CoroutineScope 創(chuàng)建了協(xié)程。我們目前對這個 CoroutineScope 一無所知,它可能是viewModelScope 或者是其他地方定義的某個 CoroutineScope,但不管怎樣,coroutineScope 構(gòu)造器都會使用它作為其創(chuàng)建新的 scope 的父級。
然后,在 coroutineScope 代碼塊內(nèi),launch 將會在新的 scope 中啟動協(xié)程,隨著協(xié)程的啟動完成,scope 會對其進行追蹤。最后,一旦所有在 coroutineScope 內(nèi)啟動的協(xié)程都完成后,loadLots 方法就可以輕松地返回了。
注意:scope 和協(xié)程之間的父子關(guān)系是使用 Job 對象進行創(chuàng)建的。但是您不需要深入去了解,只要知道這一點就可以了。
coroutineScope 和 supervisorScope 將會等待所有的子協(xié)程都完成。
以上的重點是,使用 coroutineScope 和 supervisorScope 可以從任何 suspend function 來安全地啟動協(xié)程。即使是啟動一個新的協(xié)程,也不會出現(xiàn)泄漏,因為在新的協(xié)程完成之前,調(diào)用方始終處于掛起狀態(tài)。
更厲害的是,coroutineScope 將會創(chuàng)建一個子 scope,所以一旦父 scope 被取消,它會將取消的消息傳遞給所有新的協(xié)程。如果調(diào)用方是 viewModelScope,這一千個協(xié)程在用戶離開界面后都會自動被取消掉,非常整潔高效。
在繼續(xù)探討報錯 (error) 相關(guān)的問題之前,有必要花點時間來討論一下 supervisorScope 和 coroutineScope,它們的主要區(qū)別是當(dāng)出現(xiàn)任何一個子 scope 失敗的情況,coroutineScope 將會被取消。如果一個網(wǎng)絡(luò)請求失敗了,所有其他的請求都將被立即取消,這種需求選擇 coroutineScope。相反,如果您希望即使一個請求失敗了其他的請求也要繼續(xù),則可以使用 supervisorScope,當(dāng)一個協(xié)程失敗了,supervisorScope 是不會取消剩余子協(xié)程的。
協(xié)程失敗時發(fā)出報錯信號
在協(xié)程中,報錯信號是通過拋出異常來發(fā)出的,就像我們平常寫的函數(shù)一樣。來自 suspend 函數(shù)的異常將通過 resume 重新拋給調(diào)用方來處理。跟常規(guī)函數(shù)一樣,您不僅可以使用 try/catch 這樣的方式來處理錯誤,還可以構(gòu)建抽象來按照您喜歡的方式進行錯誤處理。
但是,在某些情況下,協(xié)程還是有可能會弄丟獲取到的錯誤的。
- val unrelatedScope = MainScope()
- // 丟失錯誤的例子
- suspend fun lostError() {
- // 未使用結(jié)構(gòu)化并發(fā)的 async
- unrelatedScope.async {
- throw InAsyncNoOneCanHearYou("except")
- }
- }
注意:上述代碼聲明了一個無關(guān)聯(lián)協(xié)程作用域,它將不會按照結(jié)構(gòu)化并發(fā)的方式啟動新的協(xié)程。還記得我在一開始說的結(jié)構(gòu)化并發(fā)是一系列編程語言特性和實踐指南的集合,在 suspend 函數(shù)中引入無關(guān)聯(lián)協(xié)程作用域違背了結(jié)構(gòu)化并發(fā)規(guī)則。
在這段代碼中錯誤將會丟失,因為 async 假設(shè)您最終會調(diào)用 await 并且會重新拋出異常,然而您并沒有去調(diào)用 await,所以異常就永遠在那等著被調(diào)用,那么這個錯誤就永遠不會得到處理。
結(jié)構(gòu)化并發(fā)保證當(dāng)一個協(xié)程出錯時,它的調(diào)用方或作用域會被通知到。
如果您按照結(jié)構(gòu)化并發(fā)的規(guī)范去編寫上述代碼,錯誤就會被正確地拋給調(diào)用方處理。
- suspend fun foundError() {
- coroutineScope {
- async {
- throw StructuredConcurrencyWill("throw")
- }
- }
- }
coroutineScope 不僅會等到所有子任務(wù)都完成才會結(jié)束,當(dāng)它們出錯時它也會得到通知。如果一個通過 coroutineScope 創(chuàng)建的協(xié)程拋出了異常,coroutineScope 會將其拋給調(diào)用方。因為我們用的是coroutineScope 而不是 supervisorScope,所以當(dāng)拋出異常時,它會立刻取消所有的子任務(wù)。
使用結(jié)構(gòu)化并發(fā)
在這篇文章中,我介紹了結(jié)構(gòu)化并發(fā),并展示了如何讓我們的代碼配合 Android 中的 ViewModel 來避免出現(xiàn)任務(wù)泄漏。
同樣,我還幫助您更深入去理解和使用 suspend 函數(shù),通過確保它們在函數(shù)返回之前完成任務(wù),或者是通過暴露異常來確保它們正確發(fā)出錯誤信號。
如果我們使用了不符合結(jié)構(gòu)化并發(fā)的代碼,將會很容易出現(xiàn)協(xié)程泄漏,即調(diào)用方不知如何追蹤任務(wù)的情況。這種情況下,任務(wù)是無法取消的,同樣也不能保證異常會被重新拋出來。這樣會使得我們的代碼很難理解,并可能會導(dǎo)致一些難以追蹤的 bug 出現(xiàn)。
您可以通過引入一個新的不相關(guān)的 CoroutineScope (注意是大寫的 C),或者是使用 GlobalScope 創(chuàng)建的全局作用域,但是這種方式的代碼不符合結(jié)構(gòu)化并發(fā)要求的方式。
但是當(dāng)出現(xiàn)需要協(xié)程比調(diào)用方的生命周期更長的情況時,就可能需要考慮非結(jié)構(gòu)化并發(fā)的編碼方式了,只是這種情況比較罕見。因此,使用結(jié)構(gòu)化編程來追蹤非結(jié)構(gòu)化的協(xié)程,并進行錯誤處理和任務(wù)取消,將是非常不錯的做法。
如果您之前一直未按照結(jié)構(gòu)化并發(fā)的方法編碼,一開始確實一段時間去適應(yīng)。這種結(jié)構(gòu)確實保證與 suspend 函數(shù)交互更安全,使用起來更簡單。在編碼過程中,盡可能多地使用結(jié)構(gòu)化并發(fā),這樣讓代碼更易于維護和理解。
在本文的開始列舉了結(jié)構(gòu)化并發(fā)為我們解決的三個問題:
- 取消任務(wù) —— 當(dāng)某項任務(wù)不再需要時取消它;
- 追蹤任務(wù) —— 當(dāng)任務(wù)正在執(zhí)行時,追蹤它;
- 發(fā)出錯誤信號 —— 當(dāng)協(xié)程失敗時,發(fā)出錯誤信號表明有錯誤發(fā)生。
實現(xiàn)這種結(jié)構(gòu)化并發(fā),會為我們的代碼提供一些保障:
- 作用域取消時,它內(nèi)部所有的協(xié)程也會被取消;
- suspend 函數(shù)返回時,意味著它的所有任務(wù)都已完成;
- 協(xié)程報錯時,它所在的作用域或調(diào)用方會收到報錯通知。
總結(jié)來說,結(jié)構(gòu)化并發(fā)讓我們的代碼更安全,更容易理解,還避免了出現(xiàn)任務(wù)泄漏的情況。
下一步
本篇文章,我們探討了如何在 Android 的 ViewModel 中啟動協(xié)程,以及如何在代碼中運用結(jié)構(gòu)化并發(fā),來讓我們的代碼更易于維護和理解。
本文題目:在Android開發(fā)中使用協(xié)程|上手指南
URL分享:http://www.dlmjj.cn/article/dpdjphh.html


咨詢
建站咨詢
