偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Kotlin協(xié)程用法淺析及在京東APP業(yè)務(wù)中實(shí)踐

開(kāi)發(fā) 前端
協(xié)程定義及設(shè)計(jì)的目的:協(xié)程是一種并發(fā)設(shè)計(jì)模式,是一套由 Kotlin 提供的線程框架。開(kāi)發(fā)者使用協(xié)程框架可以通過(guò)結(jié)構(gòu)化并發(fā)機(jī)制在同一作用域下,把運(yùn)行的不同線程的代碼寫在同一個(gè)代碼塊里并執(zhí)行,簡(jiǎn)化異步執(zhí)行的代碼,使得我們的代碼顯得線性。

前言

協(xié)程定義及設(shè)計(jì)的目的:協(xié)程是一種并發(fā)設(shè)計(jì)模式,是一套由 Kotlin 提供的線程框架。開(kāi)發(fā)者使用協(xié)程框架可以通過(guò)結(jié)構(gòu)化并發(fā)機(jī)制在同一作用域下,把運(yùn)行的不同線程的代碼寫在同一個(gè)代碼塊里并執(zhí)行,簡(jiǎn)化異步執(zhí)行的代碼,使得我們的代碼顯得線性。

用法淺析

本文基于kotlinx-coroutines-android V1.3.8版本協(xié)程庫(kù)進(jìn)行講解。

基礎(chǔ)概念

使用協(xié)程前我們需要先了解幾個(gè)概念:

  • 協(xié)程作用域 CoroutineScope:定義新協(xié)程的范圍,通過(guò)它的擴(kuò)展函數(shù)可以創(chuàng)建、啟動(dòng)協(xié)程,并可以管理協(xié)程,比如取消該作用域下的協(xié)程,Kotlin 協(xié)程為我們提供了一組內(nèi)置的 Scope: MainScope:使用 Dispatchers.Main 調(diào)度器的作用域 LifecycleScope:與 Lifecycle 生命周期綁定 ViewModelScope:與 ViewModel 生命周期綁定 GlobalScope:生命周期貫穿全局
  • 協(xié)程構(gòu)建器:CoroutineScope 的擴(kuò)展函數(shù),用于構(gòu)建協(xié)程,比如 launch,async;
  • 協(xié)程上下文 CoroutineContext:一個(gè)左向鏈表的實(shí)現(xiàn),Job、Dispatcher 調(diào)度器都可以是它的元素,CoroutineContext 有一個(gè)非常好的作用就是可以很方便的通過(guò)其獲取 Job、Dispatcher 調(diào)度器等數(shù);
  • CoroutineStart啟動(dòng)模式:DEFAULT:立即調(diào)度,可以在執(zhí)行前被取消 LAZY:需要時(shí)才啟動(dòng),需要 start、join 等函數(shù)觸發(fā)才可進(jìn)行調(diào)度 ATOMIC:立即調(diào)度,協(xié)程肯定會(huì)執(zhí)行,執(zhí)行前不可以被取消 UNDISPATCHED:立即在當(dāng)前線程執(zhí)行,直到遇到第一個(gè)掛起
  • Dispatchers調(diào)度器:DEFAULT:默認(rèn)調(diào)度器,適合 CPU 密集型任務(wù)調(diào)度器,比如邏輯計(jì)算 Main:UI 線程調(diào)度器 Unconfined:對(duì)協(xié)程執(zhí)行的線程不做限制,協(xié)程恢復(fù)時(shí)可以在任意線程 IO:IO調(diào)度器,適合 IO 密集型任務(wù)調(diào)度器 比如讀寫文件,網(wǎng)絡(luò)請(qǐng)求等
  • suspending lambda:一個(gè)可掛起的 lambda 表達(dá)式,它的全定義為 suspend CoroutineScope.() -> Unit,這是一個(gè)被 suspend 修飾符修飾的"CoroutineScope 擴(kuò)展函數(shù)類型",作為擴(kuò)展函數(shù),它的優(yōu)勢(shì)在于可以直接訪問(wèn) CoroutineScope 內(nèi)的屬性;
  • suspension point 掛起點(diǎn):一般對(duì)應(yīng)掛起函數(shù)被調(diào)用的位置;
  • 掛起函數(shù):由 suspend 修飾的函數(shù),掛起函數(shù)只能在掛起函數(shù)或者協(xié)程中調(diào)用;

協(xié)程的創(chuàng)建與啟動(dòng)

開(kāi)篇中概念章節(jié)中介紹了協(xié)程構(gòu)建器用于協(xié)程的構(gòu)建,協(xié)程的構(gòu)建器是CoroutineScope的擴(kuò)展函數(shù)。

launch

  1. coroutineScope.launch(Dispatchers.IO) { // 示例(1) 
  2.     // 運(yùn)行在IO線程 
  3. coroutineScope.launch(Dispatchers.Main) { // 示例(2) 
  4.     // 運(yùn)行在UI線程 

在上述代碼中,演示了一個(gè)協(xié)程的創(chuàng)建,我們以實(shí)例(1)為例,它的含義是通過(guò) coroutineScope 作用域的擴(kuò)展函數(shù) launch 創(chuàng)建了一個(gè)運(yùn)行在IO線程的協(xié)程,大家可以看到代碼還是很清晰的,這時(shí)候就可以在協(xié)程中做一些耗時(shí)性的操作。同理實(shí)例(2)中創(chuàng)建了一個(gè)運(yùn)行在UI線程的協(xié)程。

  1. val job: Job = coroutineScope.launch(Dispatchers.IO, CoroutineStart.LAZY) { // 示例(1) 
  2.                     // 運(yùn)行在IO 
  3.                } 
  4.                job.start() 

在上述代碼中,我們將示例(1)進(jìn)行了改造,調(diào)用 launch 函數(shù)時(shí),新增了一個(gè)參數(shù) CoroutineStart.LAZY,并將返回的 Job 對(duì)象賦值給變量 job。

默認(rèn)情況下,協(xié)程的啟動(dòng)模式為 CoroutineStart.DEFAULT,即協(xié)程創(chuàng)建完成之后會(huì)立即執(zhí)行,示例中設(shè)置啟動(dòng)模式為 CoroutineStart.LAZY,這時(shí)候 launch 函數(shù)創(chuàng)建了協(xié)程,并沒(méi)有啟動(dòng)它,此時(shí)協(xié)程的啟動(dòng)需要依靠 Job 的 start 等函數(shù)進(jìn)行啟動(dòng)。

Job 是一個(gè)具有生命周期的并且可以被取消的后臺(tái)工作或者說(shuō)異步任務(wù),Job 內(nèi)提供了 isActive、isCompleted、isCancelled 屬性用以判斷協(xié)程的狀態(tài),以及啟動(dòng)協(xié)程 start()、取消協(xié)程 cancel() 等操作的 api。

async并發(fā)

假如現(xiàn)在有這個(gè)一個(gè)需求,存在兩個(gè)接口,一個(gè)用于獲取用戶個(gè)人信息、一個(gè)用于獲取企業(yè)信息,需要兩個(gè)接口數(shù)據(jù)都獲取到的時(shí)候才可以進(jìn)行 UI 的刷新,這時(shí)候 async 并發(fā)就凸顯它的優(yōu)勢(shì);

  1. coroutineScope.launch(Dispatchers.Main) { 
  2.     val async1 = async(Dispatchers.IO) { // 網(wǎng)絡(luò)請(qǐng)求1 
  3.         "模擬用戶信息數(shù)據(jù)獲取" 
  4.     } 
  5.     val async2 = async(Dispatchers.IO) { // 網(wǎng)絡(luò)請(qǐng)求2 
  6.         "模擬企業(yè)信息數(shù)據(jù)獲取" 
  7.     } 
  8.     handleData(async1.await(), async2.await()) // 模擬合并數(shù)據(jù) 

在上述代碼中通過(guò) async 發(fā)起兩個(gè)協(xié)程獲取數(shù)據(jù),并通過(guò) await() 獲取到請(qǐng)求結(jié)果,因?yàn)椴⑿邪l(fā)起,所以速度也是挺快的。

通過(guò) async 創(chuàng)建的協(xié)程返回值是一個(gè) Deferred,Deferred 帶有延遲的意思,可以通俗理解成要等一等才能拿到結(jié)果,Deferred 也是一個(gè) Job,它是 Job 的一個(gè)子類,所以具有 Job 同樣的功能。

當(dāng)然 async 默認(rèn)的啟動(dòng)模式和 launch 一樣,也是 CoroutineStart.DEFAULT 立即執(zhí)行,當(dāng)將啟動(dòng)模式設(shè)置為 CoroutineStart.LAZY 時(shí)可以通過(guò) await() 啟動(dòng)協(xié)程,也可以通過(guò) Job 的 start() 函數(shù)啟動(dòng)。

Kotlin協(xié)程優(yōu)勢(shì)

在這一章節(jié)中,會(huì)通過(guò)幾個(gè)示例對(duì)比,來(lái)體現(xiàn)Kotlin協(xié)程的優(yōu)勢(shì)在哪里,同時(shí)筆者建議閱讀此章節(jié)的時(shí)候不要太在意實(shí)現(xiàn)的細(xì)節(jié),關(guān)注不同方式的實(shí)現(xiàn)風(fēng)格就好。

  1. /** 獲取用戶信息 */ 
  2. private fun getUserInfo() { // 示例(1) 
  3.     apiService.getUserInfo().enqueue(object : Callback<UserInfoEntry> { 
  4.         override fun onResponse(c: Call<UserInfoEntry>, re: Response<UserInfoEntry>) { 
  5.             runOnUiThread { 
  6.                 tvName.text = response.body()?.userName 
  7.             } 
  8.         } 
  9.  
  10.         override fun onFailure(call: Call<UserInfoEntry>, t: Throwable) { 
  11.         } 
  12.     }) 
  13.  
  14. /** 獲取用戶信息 協(xié)程*/ 
  15. private fun getUserInfoByCoroutine() { // 示例(2) 
  16.     coroutineScope.launch(Dispatchers.Main) { 
  17.         val userInfo = coroutineApiService.getUserInfo() 
  18.         tvName.text = userInfo.userName 
  19.     } 

這是一個(gè)獲取用戶信息的網(wǎng)絡(luò)請(qǐng)求示例,通過(guò)普通的 CallBack 方式及 Kotlin協(xié) 程的方式分別實(shí)現(xiàn)。

  • 示例(1)是比較常見(jiàn)的一個(gè)種方式,發(fā)起網(wǎng)絡(luò)請(qǐng)求,通過(guò) CallBack 回調(diào)數(shù)據(jù),最后切換主線程刷新 UI,很常見(jiàn)的寫法。
  • 示例(2)是協(xié)程的實(shí)現(xiàn)方式,通過(guò) scope 的擴(kuò)展函數(shù) launch 創(chuàng)建了一個(gè)運(yùn)行在主線程的協(xié)程,協(xié)程的實(shí)現(xiàn)中,也是獲取數(shù)據(jù)后刷新 UI。

現(xiàn)在我們對(duì)比一下兩種方式的實(shí)現(xiàn),看看協(xié)程的實(shí)現(xiàn)有什么優(yōu)化的地方?首先在協(xié)程的實(shí)現(xiàn)中沒(méi)有了 CallBack 的回調(diào),其次在刷新UI的時(shí)候并沒(méi)有切換到主線程的操作,最后代碼量也是比較簡(jiǎn)潔的。

其實(shí)還好,第一種方式在我們?cè)陂_(kāi)發(fā)中,這種 CallBack 的回調(diào),應(yīng)該應(yīng)用過(guò)無(wú)數(shù)次了,寫起來(lái)也是分分鐘的事情,并不會(huì)多么困難。確實(shí),這樣 Kotlin 協(xié)程的優(yōu)勢(shì)也不是那么明顯了。

接下來(lái)我們看一個(gè)復(fù)雜一些的場(chǎng)景,以上文講解 async 時(shí)提到過(guò)的合并用戶信息數(shù)據(jù)和企業(yè)信息數(shù)據(jù)為例,我們看看更詳細(xì)的實(shí)現(xiàn),在這里復(fù)述一下場(chǎng)景:“存在兩個(gè)接口,一個(gè)用于獲取用戶個(gè)人信息、一個(gè)用于獲取企業(yè)信息,需要兩個(gè)接口數(shù)據(jù)都獲取到的時(shí)候才可以進(jìn)行 UI 的刷新”。

普通方式

  1.      
  2. /** 開(kāi)始獲取數(shù)據(jù) */ 
  3.   private fun start() { 
  4.       getUserInfo() 
  5.       getCompanyInfo() 
  6.   } 
  7.  
  8. /** 獲取用戶信息 */ 
  9.   private fun getUserInfo() { 
  10.       apiService.getUserInfo().enqueue(object : Callback<UserInfoEntry> { 
  11.           override fun onResponse(c: Call<UserInfoEntry>, r: Response<UserInfoEntry>) { 
  12.               // 判斷是不是已經(jīng)拿到公司信息了 
  13.               // 刷新UI handle.post() 
  14.           } 
  15.  
  16.           override fun onFailure(call: Call<UserInfoEntry>, t: Throwable) { 
  17.           } 
  18.       }) 
  19.   } 
  20.  
  21.   /** 獲取公司信息 */ 
  22.   private fun getCompanyInfo() { 
  23.       apiService.getCompanyInfo().enqueue(object : Callback<UserInfoEntry> { 
  24.           override fun onResponse(c: Call<UserInfoEntry>, r: Response<UserInfoEntry>) { 
  25.               // 判斷是不是已經(jīng)拿到用戶信息了 
  26.               // 刷新UI handle.post() 
  27.           } 
  28.          
  29.           override fun onFailure(call: Call<UserInfoEntry>, t: Throwable) { 
  30.           } 
  31.       }) 
  32.   } 

在這種方式中,我們將兩個(gè)接口請(qǐng)求封裝了兩個(gè) API,同時(shí)發(fā)起網(wǎng)絡(luò)請(qǐng)求,相對(duì)使用上不能說(shuō)不方便,關(guān)鍵在于數(shù)據(jù)的處理上,用戶信息的數(shù)據(jù)拿到之后需要判斷企業(yè)信息是不是也獲取到了,同理企業(yè)信息的數(shù)據(jù)也是一樣,現(xiàn)在只有兩組數(shù)據(jù)的合并,如果涉及更多信息類型數(shù)據(jù)的獲取,相應(yīng)的邏輯處理就變的越來(lái)越復(fù)雜了。

當(dāng)然如果改成串行的邏輯也是很好處理的,比如先獲取用戶信息數(shù)據(jù),獲取之后再進(jìn)行企業(yè)信息數(shù)據(jù)的讀取,但是這種方式犧牲了時(shí)間,本來(lái)可以并行的請(qǐng)求,變成串行,請(qǐng)求時(shí)間加長(zhǎng)。

Kotlin協(xié)程

  1. /** 獲取信息 kotlin協(xié)程 */ 
  2. private fun getKotlinInfo() { 
  3.     coroutineScope.launch(Dispatchers.Main) { 
  4.         val userInfo = async { 
  5.             apiService.getUserInfo() 
  6.         } // 獲取用戶信息 
  7.         val companyInfo = async { 
  8.             apiService.getCompanyInfo() 
  9.         } // 公司信息 
  10.         MergeEntry(userInfo.await(), companyInfo.await()) 
  11.     } 

這是 Kotlin 協(xié)程的實(shí)現(xiàn)方式,使用 CoroutineScope 的 async 構(gòu)建器實(shí)現(xiàn),在需要更多請(qǐng)求時(shí),它的邏輯處理很方便,多一個(gè)請(qǐng)求多一個(gè) async 即可,并行的請(qǐng)求節(jié)省時(shí)間,而且消除了回調(diào),并且不需要切換線程。

協(xié)程的使用

在了解了協(xié)程的創(chuàng)建、啟動(dòng)及優(yōu)勢(shì)之后,現(xiàn)在有一個(gè)問(wèn)題我們什么時(shí)候使用協(xié)程?當(dāng)我們需要處理耗時(shí)數(shù)據(jù)的時(shí)候,這時(shí)候可以使用協(xié)程切換到子線程執(zhí)行,當(dāng)處理完數(shù)據(jù)需要刷新 UI 的時(shí)候可以使用協(xié)程切換到主線程,其實(shí)需要指定運(yùn)行線程的時(shí)候就可以用協(xié)程處理。

  1. coroutineScope.launch(Dispatchers.IO) { // 運(yùn)行在IO線程 
  2.     handleFileData() // 模擬讀文件耗時(shí)操作 
  3.     launch(Dispatchers.Main) { // 數(shù)據(jù)處理完成刷新UI 
  4.         tvName.text = "" 
  5.     } 

在上述代碼中,有一個(gè)耗時(shí)讀文件操作,所以這里使用了協(xié)程,通過(guò) launch 切換到 IO 線程處理耗時(shí)操作,處理完成之后通過(guò) launch 函數(shù)切到 Main 線程刷新 UI,好像沒(méi)毛病,我們繼續(xù)看下一段代碼。

  1. coroutineScope.launch(Dispatchers.IO) {// 運(yùn)行在IO線程 
  2.      handleFileData() // 模擬讀文件 
  3.      launch(Dispatchers.Main) { 
  4.          // 數(shù)據(jù)處理完成刷新UI 
  5.          launch(Dispatchers.IO) { 
  6.              // 處理數(shù)據(jù) 
  7.              launch(Dispatchers.Main) { 
  8.                  // 數(shù)據(jù)處理完成刷新UI 
  9.                  launch(Dispatchers.IO) { 
  10.                      launch(Dispatchers.Main) { 
  11.                          launch(Dispatchers.IO) { 
  12.                              launch(Dispatchers.Main) { 
  13.                              } 
  14.                          } 
  15.                      } 
  16.                  } 
  17.              } 
  18.          } 
  19.      } 
  20.  } 

這個(gè)示例演示的場(chǎng)景比較極端,很少在開(kāi)發(fā)中會(huì)遇到 IO 與 Main 線程切換如此頻繁,在這里只是為了暴露問(wèn)題。前面我們說(shuō)過(guò) Kolin 協(xié)程消除了回調(diào),但在這個(gè)示例中卻表現(xiàn)的很回調(diào),層層嵌套。

因?yàn)閱螁问褂?launch、async 協(xié)程構(gòu)建器函數(shù)并不能很好的處理這種復(fù)雜的需要頻繁切換線程的場(chǎng)景,為了解決示例中的問(wèn)題,Kotlin 協(xié)程為我們提供了一些另外的函數(shù)來(lái)配合使用, 比如 withContext 掛起函數(shù)。

withContext 掛起函數(shù)

withContext 是 Kotlin 協(xié)程提供的掛起函數(shù),它提供給的功能有:

  • 可以切換到指定的線程運(yùn)行;
  • 函數(shù)體執(zhí)行完之后,自動(dòng)切回原來(lái)的線程。
  1. coroutineScope.launch(Dispatchers.Main) { // 在主線程開(kāi)啟一個(gè)協(xié)程 
  2.     val data = withContext(Dispatchers.IO) { // 切到IO線程處理耗時(shí)操作 
  3.         handleFileData() // 在IO線程運(yùn)行 
  4.     } 
  5.     tvName.text = data // withContext函數(shù)體執(zhí)行完,自定切換到主線程刷新UI 
  6.  
  7. coroutineScope.launch(Dispatchers.Main) { 
  8.     withContext(Dispatchers.IO) { // **操作(1)** 
  9.         // 切換IO線程 
  10.         // ... 在IO線程執(zhí)行 
  11.     } 
  12.     // .. 在UI線程執(zhí)行  **操作(2)** 
  13.     withContext(Dispatchers.IO) { 
  14.         // 切換IO線程 
  15.         // ... 在IO線程執(zhí)行 
  16.     } 
  17.     // .. 在UI線程執(zhí)行 
  18.     withContext(Dispatchers.IO) { 
  19.         // 切換IO線程 
  20.         // ... 在IO線程執(zhí)行 
  21.     } 
  22.     // .. 在UI線程執(zhí)行 
  23.     // ...等等... 

使用 withContext 改造之后,消除了嵌套,代碼變得清晰,所以,Kotlin 協(xié)程除了 launch 等擴(kuò)展函數(shù)之外,還需要 withContext 等掛起函數(shù),才可體現(xiàn)它的優(yōu)勢(shì)。

這里有必要提一下,在沒(méi)有使用協(xié)程的時(shí)候,開(kāi)啟一個(gè)線程,代碼就會(huì)出現(xiàn)兩個(gè)分支,比如上述代碼中的操作(1),切到了IO線程執(zhí)行,這是一個(gè)分支,緊接著是執(zhí)行操作(2),這是另一個(gè)分支,這兩個(gè)分支各走各的,“幾乎同步執(zhí)行”;

但在協(xié)程中,操作(1)使用withContext掛起函數(shù)切換到IO線程去執(zhí)行它的操作后,并不會(huì)執(zhí)行操作(2),而是等待操作(1)的withContext執(zhí)行完成之后,切換線程回到Main線程中時(shí),操作(2)才會(huì)執(zhí)行,后續(xù)的supend章節(jié)會(huì)有講解。

  1. public suspend fun <T> withContext( 
  2.     context: CoroutineContext, 
  3.     block: suspend CoroutineScope.() -> T 
  4. ): T {} 

在上面的示例中 getData() 是一個(gè)普通的函數(shù),在其中調(diào)用的 withContext 掛起函數(shù)時(shí),提示報(bào)錯(cuò)信息:suspend function 'withContext' should be called only from a coroutine or another supend function,意思是說(shuō) withContext 是一個(gè)被 suspend 修飾的函數(shù),它應(yīng)該在協(xié)程或者另一個(gè) spspend 函數(shù)中調(diào)用。源碼中 withContext 被 suspend 修飾。

suspend

suspend 是 Kotlin 協(xié)程的一個(gè)關(guān)鍵字,由 suspend 修飾的函數(shù)為掛起函數(shù),掛起函數(shù)只能在協(xié)程或者另一個(gè)掛起函數(shù)中調(diào)用。

  • 從開(kāi)發(fā)者的層面說(shuō),suspend 關(guān)鍵字的作用就是一個(gè)提醒的作用,提醒什么?提醒函數(shù)的調(diào)用者,這是一個(gè)掛起函數(shù),內(nèi)部存在耗時(shí)操作,需要在協(xié)程或者另一個(gè)掛起函數(shù)中調(diào)用才行;
  • 但從編譯過(guò)程來(lái)說(shuō),被 suspend 修飾的函數(shù),有特殊的解讀,比如會(huì)新增一個(gè)參數(shù) Continuation,這也是為什么在普通函數(shù)中無(wú)法調(diào)用掛起函數(shù)的原因。

掛起函數(shù)?掛起的是誰(shuí)?

剛才我們說(shuō)被 suspend 修飾的函數(shù)是掛起函數(shù),掛起從字面意思可以理解為不執(zhí)行了或者說(shuō)是暫停了,這里有一個(gè)疑問(wèn),掛起的是誰(shuí)?是線程?函數(shù)?還是協(xié)程?

其實(shí)掛起的是協(xié)程,可以理解為在協(xié)程中執(zhí)行到 suspend 掛起函數(shù)的時(shí)候,就會(huì)暫停協(xié)程后續(xù)代碼的執(zhí)行,我們分析一下下面代碼的執(zhí)行流程。

  1. coroutineScope.launch(Dispatchers.Main) { // 在主線程開(kāi)啟一個(gè)協(xié)程  (1) 
  2.     val data = withContext(Dispatchers.IO) { // 切到IO線程處理耗時(shí)操作 (2) 
  3.         handleFileData() // 在IO線程運(yùn)行 (3) 
  4.     } 
  5.     tvName.text = data // withContext函數(shù)體執(zhí)行完,自定切換到主線程刷新UI (4) 

通過(guò) CoroutineScope 的擴(kuò)展函數(shù) launch 啟動(dòng)了一個(gè)運(yùn)行在 Main 線程的協(xié)程,當(dāng)協(xié)程執(zhí)行到 withContext 掛起函數(shù)的時(shí)候,withCotext 切到的 IO 線程,執(zhí)行自身函數(shù)體的耗時(shí)操作,同時(shí)協(xié)程后續(xù)的代碼就會(huì)暫停執(zhí)行,這里也是協(xié)程最神奇的地方。

那么后續(xù)的代碼什么時(shí)候執(zhí)行?協(xié)程掛起了,對(duì)應(yīng)的也有恢復(fù)的操作,這里就涉及協(xié)程的恢復(fù)了,當(dāng) withContext 掛起函數(shù)執(zhí)行完成之后,協(xié)程會(huì)重新切回原來(lái)的線程(如果掛起前的線程是一個(gè)子線程,有可能會(huì)因?yàn)榫€程空閑而被回收,切回來(lái)的線程并不一定百分百是原來(lái)的線程)繼續(xù)執(zhí)行剩余的代碼,比如示例中刷新UI的操作。

總結(jié)一下 Kotlin 協(xié)程掛起的概念,什么是掛起?可以理解為兩個(gè)操作:

  • 協(xié)程被“暫停”執(zhí)行;
  • 協(xié)程被“恢復(fù)”執(zhí)行。

更通俗一些,當(dāng) Kotlin 協(xié)程執(zhí)行到一個(gè)掛起函數(shù)時(shí),會(huì)將線程切換到掛起函數(shù)指定的線程中執(zhí)行,后續(xù)的代碼將被暫停執(zhí)行,當(dāng)掛起函數(shù)執(zhí)行完成之后,會(huì)將線程重新切回原來(lái)的線程,恢復(fù)剩余代碼的執(zhí)行,這就是掛起。

另外說(shuō)一下掛起的非阻塞式:

還是以上面的代碼為例,操作(1)在 Main 線程中啟動(dòng)了一個(gè)協(xié)程,協(xié)程執(zhí)行到操作(2)時(shí),切到 IO 線程中執(zhí)行操作(3),此時(shí)操作(4)被暫停,不執(zhí)行了,但 Main 線程被阻塞了嗎?并沒(méi)有,主線程該干嘛就干嘛去了,這就是掛起的非阻塞式,雖然被掛起了,但掛起的是自己,是協(xié)程,并沒(méi)有阻塞原來(lái)的線程。

京東APP業(yè)務(wù)實(shí)踐

業(yè)務(wù)背景

本文以核心樓層數(shù)據(jù)處理進(jìn)行講解,該業(yè)務(wù)需要將兜底數(shù)據(jù)和接口下發(fā)的動(dòng)態(tài)數(shù)據(jù)進(jìn)行組裝,最終整合成業(yè)務(wù)所需的數(shù)據(jù)源。

gradle依賴配置

  1. dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8' } 

代碼實(shí)現(xiàn)

  1. /** 協(xié)程作用域 */ 
  2. private val scope = MainScope() 
  3.  
  4. private fun assembleDataList(response: PlatformResponse?) = scope.launch( 
  5.             CoroutineExceptionHandler { _, exception -> 
  6.                 /** 未捕獲的異常處理 */ 
  7.             }) 
  8.     { 
  9.         val localStaticData: Deferred<MutableList<BaseTemplateEntity>?> = async(start = CoroutineStart.LAZY) { getLocalStaticData() } 
  10.         val dynamicData: Deferred<MutableList<BaseTemplateEntity>?> = async(start = CoroutineStart.LAZY) { getDynamicData(response) } 
  11.         getAssembleDataListFunc(localStaticData.await(), dynamicData.await()) 
  12.     } 

我們通過(guò)作用域構(gòu)建器擴(kuò)展函數(shù) launch 在當(dāng)前的 MainScope 下創(chuàng)建新的協(xié)程并啟動(dòng),在 launch 函數(shù)的 lambda 表達(dá)式中,我們使用了 async 函數(shù)并聲明 start 參數(shù)設(shè)置為 CoroutineStart.LAZY 惰性模式創(chuàng)建一個(gè)子協(xié)程(但該協(xié)程并不會(huì)立即執(zhí)行),該函數(shù)會(huì)返回一個(gè) Deferred 對(duì)象,Deferred 是帶有返回值的 Job 擴(kuò)展(類似于 Java 中的 Futuer 對(duì)象),只有當(dāng)我們主動(dòng)調(diào)用 Deferred 的 await 或 start 函數(shù)時(shí),該子協(xié)程才會(huì)真正執(zhí)行。

執(zhí)行過(guò)程

與 RxJava 實(shí)現(xiàn)方案對(duì)比

RxJava 實(shí)現(xiàn)

  1. private void assembleDataList(PlatformResponse response) { 
  2.     Observable<List<BaseTemplateEntity>> localStaticData = getLocalStaticData(); 
  3.     Observable<List<BaseTemplateEntity>> assembleData = getDynamicData(response); 
  4.     Func2<List<BaseTemplateEntity>, List<BaseTemplateEntity>, List<BaseTemplateEntity>> assembleData = getAssembleDataListFunc(); 
  5.     Observable<List<BaseTemplateEntity>> observable = Observable.zip(localStaticData, assembleData, assembleData); 
  6.         subscribe(observable, callback); 
  7.     } 

通過(guò)實(shí)現(xiàn)代碼可以看出,我們使用 zip 操作符,將 localStaticData 和 assembleData 這兩個(gè)觀察者發(fā)送的事件序列,在組合后生成一個(gè)新的事件序列并發(fā)送(此處我們不討論 localStaticData 和 assembleData 這兩個(gè)事件序列是串行還是并行執(zhí)行)。

  • zip操作符事件流向圖(圖片來(lái)自ReactiveX官網(wǎng))

  • 對(duì)比 針對(duì)我們的業(yè)務(wù)場(chǎng)景,協(xié)程和 RxJava 實(shí)現(xiàn)方式都能滿足我們的需求,那他們之前有什么區(qū)別呢:我們先來(lái)說(shuō)一說(shuō) RxJava 的優(yōu)點(diǎn):解決了 Java 異步實(shí)現(xiàn)回調(diào)嵌套問(wèn)題,提高了代碼的可讀性及維護(hù)性;鏈?zhǔn)秸{(diào)用將事件的配置階段、運(yùn)行階段、訂閱階段的調(diào)用變得扁平化;線程調(diào)度使得切換線程輕松又優(yōu)雅。RxJava 的缺點(diǎn):
  1. Observable firstObservable = Observable.create(new Observable.OnSubscribe<CacheBean>() { 
  2.             @Override 
  3.             public void call(Subscriber<? super CacheBean> subscriber) { 
  4.                 if (subscriber != null && !subscriber.isUnsubscribed()) { 
  5.                     subscriber.onNext(handleCacheBean()); 
  6.                     subscriber.onCompleted(); 
  7.                     RxUtil.unSubscribeSafely(subscriber); 
  8.                 } 
  9.             } 
  10.         }); 
  11. Observable secondObservable = Observable.just(new CacheBean(null"0")); 
  12. firstObservable.timeout(TIME_OUT, TimeUnit.SECONDS) 
  13.                .onErrorResumeNext(secondObservable) 
  14.                .subscribe(); 
  • RxJava 的行為并不可預(yù)期,太容易出錯(cuò)。如上所示示例中,如果 firstObservable 運(yùn)行時(shí)超時(shí)并不會(huì)結(jié)束 firstObservable 的序列繼續(xù)發(fā)射,如果不調(diào)用其 onCompleted() 事件,你會(huì)發(fā)現(xiàn)訂閱事件會(huì)先后有接收到2次不同的事件序列,而非我們希望的當(dāng)超時(shí)后只訂閱到 secondObservable 發(fā)射的事件序列。
  • RxJava 門檻太高。大部分開(kāi)發(fā)者可能不會(huì)過(guò)多深入研究,但是如果不了解這些,那么而幾乎可以說(shuō)不可能融會(huì)貫通 RxJava 的一些概念,這也就增加了學(xué)習(xí)成本及維護(hù)成本。
  • 背壓策略難以理解。
  • 堆棧日志可讀性差,增加開(kāi)發(fā)調(diào)試成本。

協(xié)程的優(yōu)點(diǎn):用同步的方式寫異步執(zhí)行的代碼,使得代碼邏輯更加簡(jiǎn)潔和清晰;輕量級(jí),占用更少的系統(tǒng)資源;執(zhí)行效率高;掛起函數(shù)較于實(shí)現(xiàn) Runnable 或 Callable 接口更加方便可控;線程切換很簡(jiǎn)單。協(xié)程的缺點(diǎn):有一定學(xué)習(xí)成本,由于是基于 Kotlin 語(yǔ)言,需有一定語(yǔ)言基礎(chǔ)。

協(xié)程和 RxJava ,我們應(yīng)該如何選擇?

經(jīng)過(guò)協(xié)程和 RxJava 的對(duì)比,我們也對(duì)各框架有所了解,但談到應(yīng)該如何選擇這個(gè)話題,筆者以為如果你已經(jīng)對(duì) RxJava 重度使用,其實(shí)沒(méi)必要刻意遷移到協(xié)程,RxJava 功能強(qiáng)大目前仍是很流行的異步編程框架,基于 RxJava 的拓展庫(kù) RxKotlin 也可以滿足在 kotlin 語(yǔ)言環(huán)境下使用 RxJava 開(kāi)發(fā)。如果你已經(jīng)有一定 Kotlin 開(kāi)發(fā)經(jīng)驗(yàn),又喜歡嘗試新鮮事物,協(xié)程是個(gè)不錯(cuò)的選擇,其非阻塞時(shí)的掛起可以讓開(kāi)發(fā)人員用同步的風(fēng)格編寫異步代碼,提高開(kāi)發(fā)效率同時(shí)也降低了維護(hù)成本。協(xié)程的概念越來(lái)越普及,尤其已在 Flutter 跨平臺(tái)框架中廣泛使用,勢(shì)必會(huì)成為趨勢(shì)。

 

責(zé)任編輯:未麗燕 來(lái)源: 京東零售技術(shù)
相關(guān)推薦

2023-10-24 19:37:34

協(xié)程Java

2021-05-20 09:14:09

Kotlin協(xié)程掛起和恢復(fù)

2020-06-19 08:01:48

Kotlin 協(xié)程編程

2017-06-15 13:15:39

Python協(xié)程

2020-02-19 14:16:23

kotlin協(xié)程代碼

2025-05-16 08:21:45

2019-10-23 14:34:15

KotlinAndroid協(xié)程

2022-06-30 09:30:36

FlinkSQL流批一體京東

2021-09-16 09:59:13

PythonJavaScript代碼

2022-04-02 09:57:51

技術(shù)京東實(shí)踐

2021-06-22 16:21:40

鴻蒙HarmonyOS應(yīng)用

2020-12-04 14:32:33

AndroidJetpackKotlin

2021-05-13 21:58:00

高并發(fā)應(yīng)用Asyncio

2009-08-27 16:00:59

C#中using用法

2021-09-14 19:01:56

ClickHouse京東小程序

2023-11-17 11:36:59

協(xié)程纖程操作系統(tǒng)

2020-10-20 10:31:13

JSGenerator協(xié)程

2025-06-26 04:10:00

2023-09-03 19:13:29

AndroidKotlin

2021-12-09 06:41:56

Python協(xié)程多并發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)