<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Kotlin Vocabulary | 揭秘協(xié)程中的 suspend 修飾符

          共 11078字,需瀏覽 23分鐘

           ·

          2020-07-10 18:24

          1d24b3f2bc74debf4909b51966ecc33e.webpKotlin 協(xié)程把 suspend 修飾符引入到了我們 Android 開發(fā)者的日常開發(fā)中。您是否好奇它的底層工作原理呢?編譯器是如何轉(zhuǎn)換我們的代碼,使其能夠掛起和恢復(fù)協(xié)程操作的呢?
          了解這些將會幫您更好地理解掛起函數(shù) (suspend function) 為什么只會在所有工作完成后才會返回,以及如何在不阻塞線程的情況下掛起代碼。
          本文概要: Kotlin 編譯器將會為每個掛起函數(shù)創(chuàng)建一個狀態(tài)機(jī),這個狀態(tài)機(jī)將為我們管理協(xié)程的操作!


          ? 如果您是 Android 平臺上協(xié)程的初學(xué)者,請查閱下面這些協(xié)程 codelab:

          • 在 Android 應(yīng)用中使用協(xié)程https://codelabs.developers.google.com/codelabs/kotlin-coroutines/#0
          • 協(xié)程的進(jìn)階使用: Kotlin Flow 和 Live Datahttps://codelabs.developers.google.com/codelabs/advanced-kotlin-coroutines/#0


          協(xié)程 101


          協(xié)程簡化了 Android 平臺的異步操作。正如官方文檔《利用 Kotlin 協(xié)程提升應(yīng)用性能》所介紹的,我們可以使用協(xié)程管理那些以往可能阻塞主線程或者讓應(yīng)用卡死的異步任務(wù)。


          • 《利用 Kotlin 協(xié)程提升應(yīng)用性能》

            https://developer.android.google.cn/kotlin/coroutines


          協(xié)程也可以幫我們用命令式代碼替換那些基于回調(diào)的 API。例如,下面這段使用了回調(diào)的異步代碼:

          // 簡化的只考慮了基礎(chǔ)功能的代碼fun?loginUser(userId:?String,?password:?String,?userResult:?Callback<User>)?{  // 異步回調(diào)??userRemoteDataSource.logUserIn?{?user?->    // 成功的網(wǎng)絡(luò)請求????userLocalDataSource.logUserIn(user)?{?userDb?->      // 保存結(jié)果到數(shù)據(jù)庫      userResult.success(userDb)    }  }}


          上面的回調(diào)可以通過使用協(xié)程轉(zhuǎn)換為順序調(diào)用:
          suspend fun loginUser(userId: String, password: String): User {  val user = userRemoteDataSource.logUserIn(userId, password)  val userDb = userLocalDataSource.logUserIn(user)  return userDb}


          在后面這段代碼中,我們?yōu)楹瘮?shù)添加了 suspend 修飾符,它可以告訴編譯器,該函數(shù)需要在協(xié)程中執(zhí)行。作為開發(fā)者,您可以把掛起函數(shù)看作是普通函數(shù),只不過它可能會在某些時刻掛起和恢復(fù)而已。
          不同于回調(diào),協(xié)程提供了一種簡單的方式來實(shí)現(xiàn)線程間的切換以及對異常的處理。但是,在我們把一個函數(shù)寫成掛起函數(shù)時,編譯器在內(nèi)部究竟做了什么事呢?

          Suspend 的工作原理


          回到 loginUser 掛起函數(shù),注意它調(diào)用的另一個函數(shù)也是掛起函數(shù):
          suspend fun loginUser(userId: String, password: String): User {  val user = userRemoteDataSource.logUserIn(userId, password)  val userDb = userLocalDataSource.logUserIn(user)  return userDb}
          // UserRemoteDataSource.ktsuspend fun logUserIn(userId: String, password: String): User
          // UserLocalDataSource.ktsuspend fun logUserIn(userId: String): UserDb

          簡而言之,Kotlin 編譯器會把掛起函數(shù)使用有限狀態(tài)機(jī) (稍后講到) 轉(zhuǎn)換為一種優(yōu)化版回調(diào)。也就是說,編譯器會幫您實(shí)現(xiàn)這些回調(diào)!

          • 有限狀態(tài)機(jī)

            https://en.wikipedia.org/wiki/Finite-state_machine


          Continuation 接口

          掛起函數(shù)通過 Continuation 對象在方法間互相通信。Continuation 其實(shí)只是一個具有泛型參數(shù)和一些額外信息的回調(diào)接口,稍后我們會看到,它會實(shí)例化掛起函數(shù)所生成的狀態(tài)機(jī)。


          • Continuation

            https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-continuation/index.html

          • Continuation

            https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/coroutines/Continuation.kt


          我們先來看看它的聲明:

          interface Continuation<in T> {  public val context: CoroutineContext  public fun resumeWith(value: Result<T>)}


          • context 是 Continuation 將會使用的 CoroutineContext;

          • resumeWith 會恢復(fù)協(xié)程的執(zhí)行,同時傳入一個 Result 參數(shù),Result 中會包含導(dǎo)致掛起的計算結(jié)果或者是一個異常。


          • Result

            https://github.com/Kotlin/kotlinx.coroutines/blob/master/stdlib-stubs/src/Result.kt


          注意:?從 Kotlin 1.3 開始,您也可以使用 resumeWith 對應(yīng)的擴(kuò)展函數(shù): resume?(value: T) 和 resumeWithException?(exception: Throwable)。


          • resume

            https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/resume.html

          • resumeWithException

            https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/resume-with-exception.html


          編譯器將會在函數(shù)簽名中使用額外的 completion 參數(shù) (Continuation 類型) 來代替 suspend 修飾符。而該參數(shù)將會被用于向調(diào)用該掛起函數(shù)的協(xié)程返回結(jié)果:
          fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {  val user = userRemoteDataSource.logUserIn(userId, password)  val userDb = userLocalDataSource.logUserIn(user)  completion.resume(userDb)}


          為了簡化起見,我們的例子將會返回一個 Unit 而不是 User。User 對象將會在被加入的 Continuation 參數(shù)中 "返回"。
          其實(shí),掛起函數(shù)在字節(jié)碼中返回的是 Any。因?yàn)樗怯?T | COROUTINE_SUSPENDED 構(gòu)成的組合類型。這種實(shí)現(xiàn)可以使函數(shù)在可能的情況下同步返回。
          注意:?如果您使用 suspend 修飾符標(biāo)記了一個函數(shù),而該函數(shù)又沒有調(diào)用其它掛起函數(shù),那么編譯器會添加一個額外的 Continuation 參數(shù)但是不會用它做任何事,函數(shù)體的字節(jié)碼則會看起來和一般的函數(shù)一樣。?您也會在其他地方看到 Continuation 接口:
          • 當(dāng)使用 suspendCoroutinesuspendCancellableCoroutine ?(首選使用) 來將基于回調(diào)的 API 轉(zhuǎn)化為協(xié)程時,會直接與一個 Continuation 對象進(jìn)行交互。它會用于恢復(fù)那些執(zhí)行了參數(shù)代碼塊后掛起的協(xié)程;
          • 您可以在一個掛起函數(shù)上使用 startCoroutine 擴(kuò)展函數(shù),它會接收一個 Continuation 對象作為參數(shù),并會在新的協(xié)程結(jié)束時調(diào)用它,無論其運(yùn)行結(jié)果是成功還是異常。
          • suspendCoroutine?https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/suspend-coroutine.html
          • suspendCancellableCoroutinehttps://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html
          • startCoroutinehttps://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/start-coroutine.html

          使用不同的 Dispatcher

          您可以在不同的 Dispatcher 間切換,從而做到在不同的線程中執(zhí)行計算。那么 Kotlin 是如何知道從哪里開始恢復(fù)掛起的計算的呢?
          Continuation 有一個子類叫 DispatchedContinuation,它的 resume 函數(shù)會執(zhí)行一次調(diào)度調(diào)用,并會調(diào)度至 CoroutineContext 包含的 Dispatcher 中。除了那些將 isDispatchNeeded 方法 (會在調(diào)度前調(diào)用) 重寫為始終返回 false 的 Dispatcher.Unconfined,其他所有的 Dispatcher 都會調(diào)用 dispatch 方法。
          • DispatchedContinuationhttps://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/internal/DispatchedContinuation.kt?



          生成狀態(tài)機(jī)


          特殊說明:?本文接下來所展示的,并不是與編譯器生成的字節(jié)碼完全相同的代碼,而是足夠精確的,能夠確保您理解其內(nèi)部發(fā)生了什么的 Kotlin 代碼。這些聲明由版本為 1.3.3 的協(xié)程庫生成,可能會在其未來的版本中作出修改。


          Kotlin 編譯器會確定函數(shù)何時可以在內(nèi)部掛起,每個掛起點(diǎn)都會被聲明為有限狀態(tài)機(jī)的一個狀態(tài),每個狀態(tài)又會被編譯器用標(biāo)簽表示:
          fun?loginUser(userId:?String,?password:?String,?completion:?Continuation<Any?>)?{  // Label 0 -> 第一次執(zhí)行  val user = userRemoteDataSource.logUserIn(userId, password)  // Label 1 -> 從 userRemoteDataSource 恢復(fù)  val userDb = userLocalDataSource.logUserIn(user)  // Label 2 -> 從 userLocalDataSource 恢復(fù)  completion.resume(userDb)}


          為了更好地聲明狀態(tài)機(jī),編譯器會使用 when 語句來實(shí)現(xiàn)不同的狀態(tài):
          fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {??when(label)?{              // Label 0 -> 第一次執(zhí)行        userRemoteDataSource.logUserIn(userId, password)????}              // Label 1 -> 從 userRemoteDataSource 恢復(fù)        userLocalDataSource.logUserIn(user)????}              // Label 2 -> 從 userLocalDataSource 恢復(fù)        completion.resume(userDb)    }    else -> throw IllegalStateException(...)  }}


          這時候的代碼還不完整,因?yàn)楦鱾€狀態(tài)之間無法共享信息。編譯器會使用同一個 Continuation 對象在方法中共享信息,這也是為什么 Continuation 的泛型參數(shù)是 Any,而不是原函數(shù)的返回類型 (即 User)。

          接下來,編譯器會創(chuàng)建一個私有類,它會:

          1. 保存必要的數(shù)據(jù);

          2. 遞歸調(diào)用 loginUser 函數(shù)來恢復(fù)執(zhí)行。


          您可以查看下面提供的編譯器生成類的近似版本。


          特別說明: 注釋不是由編譯器生成的,而是由作者添加的。添加它們是為了解釋這些代碼的作用,也能讓后面的代碼更加容易理解。

          fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) { class?LoginUserStateMachine(    // completion 參數(shù)是調(diào)用了 loginUser 的函數(shù)的回調(diào)    completion: Continuation  ): CoroutineImpl(completion) {
          // suspend 的本地變量 var user: User? = null var userDb: UserDb? = null
          // 所有 CoroutineImpls 都包含的通用對象 var result: Any? = null var label: Int = 0
          // 這個方法再一次調(diào)用了 loginUser 來切換 // 狀態(tài)機(jī) (標(biāo)簽會已經(jīng)處于下一個狀態(tài)) // result 將會是前一個狀態(tài)的計算結(jié)果 override fun invokeSuspend(result: Any?) { this.result = result loginUser(null, null, this) } } ...}

          由于 invokeSuspend 函數(shù)將會再次調(diào)用 loginUser 函數(shù),并且只會傳入 Continuation 對象,所以 loginUser 函數(shù)簽名中的其他參數(shù)變成了可空類型。此時,編譯器只需要添加如何在狀態(tài)之間切換的信息。
          首先需要知道的是:
          1. 函數(shù)是第一次被調(diào)用;
          2. 函數(shù)已經(jīng)從前一個狀態(tài)中恢復(fù)。

          做到這些需要檢查 Contunuation 對象傳遞的是否是 LoginUserStateMachine 類型:
          fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {  ...
          val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
          ...}

          如果是第一次調(diào)用,它將創(chuàng)建一個新的 LoginUserStateMachine 實(shí)例,并將 completion 實(shí)例作為參數(shù)接收,以便它記得如何恢復(fù)調(diào)用當(dāng)前函數(shù)的函數(shù)。如果不是第一次調(diào)用,它將繼續(xù)執(zhí)行狀態(tài)機(jī) (掛起函數(shù))。
          現(xiàn)在,我們來看看編譯器生成的用于在狀態(tài)間切換并分享信息的代碼:
          /* Copyright 2019 Google LLC.     SPDX-License-Identifier: Apache-2.0 */fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {    ...
          val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
          when(continuation.label) {????????0?->?{ // 錯誤檢查????????????throwOnFailure(continuation.result) // 下次 continuation 被調(diào)用時, 它應(yīng)當(dāng)直接去到狀態(tài) 1????????????continuation.label?=?1 // Continuation 對象被傳入 logUserIn 函數(shù),從而可以在結(jié)束時恢復(fù) // 當(dāng)前狀態(tài)機(jī)的執(zhí)行 userRemoteDataSource.logUserIn(userId!!, password!!, continuation) }????????1?->?{ // 檢查錯誤????????????throwOnFailure(continuation.result) // 獲得前一個狀態(tài)的結(jié)果????????????continuation.user?=?continuation.result?as?User // 下次這 continuation 被調(diào)用時, 它應(yīng)當(dāng)直接去到狀態(tài) 2????????????continuation.label?=?2 // Continuation 對象被傳入 logUserIn 函數(shù),從而可以在結(jié)束時恢復(fù) // 當(dāng)前狀態(tài)機(jī)的執(zhí)行 userLocalDataSource.logUserIn(continuation.user, continuation)????????}
          ... // 故意遺漏了最后一個狀態(tài) }}

          花一些時間瀏覽上面的代碼,看看您是否能注意到與之前代碼之間的差異。下面我們來看看編譯器生成了什么:

          1. when 語句的參數(shù)是 LoginUserStateMachine 實(shí)例內(nèi)的 label;
          2. 每一次處理新的狀態(tài)時,為了防止函數(shù)被掛起時運(yùn)行失敗,都會進(jìn)行一次檢查;
          3. 在調(diào)用下一個掛起函數(shù) (即 logUserIn) 前,LoginUserStateMachine 的 label 都會更新到下一個狀態(tài);
          4. 在當(dāng)前的狀態(tài)機(jī)中調(diào)用另一個掛起函數(shù)時,continuation 的實(shí)例 (LoginUserStateMachine 類型) 會被作為參數(shù)傳遞過去。而即將被調(diào)用的掛起函數(shù)也同樣被編譯器轉(zhuǎn)換成一個相似的狀態(tài)機(jī),并且接收一個 continuation 對象作為參數(shù)。當(dāng)被調(diào)用的掛起函數(shù)的狀態(tài)機(jī)運(yùn)行結(jié)束時,它將恢復(fù)當(dāng)前狀態(tài)機(jī)的執(zhí)行。

          最后一個狀態(tài)與其他幾個不同,因?yàn)樗仨毣謴?fù)調(diào)用它的方法的執(zhí)行。如您將在下面代碼中所見,它將調(diào)用 LoginUserStateMachine 中存儲的 cont 變量的 resume 函數(shù):
          /* Copyright 2019 Google LLC.     SPDX-License-Identifier: Apache-2.0 */fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {    ...
          val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
          when(continuation.label) { ...????????2?->?{ // 錯誤檢查????????????throwOnFailure(continuation.result) // 獲取前一個狀態(tài)的結(jié)果????????????continuation.userDb?=?continuation.result?as?UserDb // 恢復(fù)調(diào)用了當(dāng)前函數(shù)的函數(shù)的執(zhí)行 continuation.cont.resume(continuation.userDb) } else -> throw IllegalStateException(...) }}


          如您所見,Kotlin 編譯器幫我們做了很多工作!例如示例中的掛起函數(shù):
          suspend fun loginUser(userId: String, password: String): User {  val user = userRemoteDataSource.logUserIn(userId, password)  val userDb = userLocalDataSource.logUserIn(user)  return userDb}

          ?

          編譯器為我們生成了下面這些代碼:
          /* Copyright 2019 Google LLC.     SPDX-License-Identifier: Apache-2.0 */fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
          ????class?LoginUserStateMachine( // completion 參數(shù)是調(diào)用了 loginUser 的函數(shù)的回調(diào) completion: Continuation????):?CoroutineImpl(completion)?{ // 要在整個掛起函數(shù)中存儲的對象 var user: User? = null var userDb: UserDb? = null // 所有 CoroutineImpls 都包含的通用對象 var result: Any? = null var label: Int = 0 // 這個函數(shù)再一次調(diào)用了 loginUser 來切換 // 狀態(tài)機(jī) (標(biāo)簽會已經(jīng)處于下一個狀態(tài)) // result 將會是前一個狀態(tài)的計算結(jié)果 override fun invokeSuspend(result: Any?) { this.result = result loginUser(null, null, this) } }
          val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion)
          when(continuation.label) {????????0?->?{ // 錯誤檢查????????????throwOnFailure(continuation.result) // 下次 continuation 被調(diào)用時, 它應(yīng)當(dāng)直接去到狀態(tài) 1????????????continuation.label?=?1 // Continuation 對象被傳入 logUserIn 函數(shù),從而可以在結(jié)束時恢復(fù) // 當(dāng)前狀態(tài)機(jī)的執(zhí)行 userRemoteDataSource.logUserIn(userId!!, password!!, continuation) }????????1?->?{ // 檢查錯誤????????????throwOnFailure(continuation.result) // 獲得前一個狀態(tài)的結(jié)果????????????continuation.user?=?continuation.result?as?User // 下次這 continuation 被調(diào)用時, 它應(yīng)當(dāng)直接去到狀態(tài) 2????????????continuation.label?=?2 // Continuation 對象被傳入 logUserIn 方法,從而可以在結(jié)束時恢復(fù) // 當(dāng)前狀態(tài)機(jī)的執(zhí)行 userLocalDataSource.logUserIn(continuation.user, continuation) }????????2?->?{ // 錯誤檢查????????????throwOnFailure(continuation.result) // 獲取前一個狀態(tài)的結(jié)果????????????continuation.userDb?=?continuation.result?as?UserDb // 恢復(fù)調(diào)用了當(dāng)前函數(shù)的執(zhí)行 continuation.cont.resume(continuation.userDb) } else -> throw IllegalStateException(...) }}

          Kotlin 編譯器將每個掛起函數(shù)轉(zhuǎn)換為一個狀態(tài)機(jī),在每次函數(shù)需要掛起時使用回調(diào)并進(jìn)行優(yōu)化。
          了解了編譯器在底層所做的工作后,您可以更好地理解為什么掛起函數(shù)會在完成所有它啟動的工作后才返回結(jié)果。同時,您也能知道 suspend 是如何做到不阻塞線程的: 當(dāng)方法被恢復(fù)時,需要被執(zhí)行的信息全部被存在了 Continuation 對象之中!


          瀏覽 29
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  热久久精品视频在线观看 | 亚洲三级在线免费观看 | 一级片黄色片 | 爽好紧别夹喷水一区二区 | 精品欧美性爱 |