<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                ## 參考文章 [Kotlin 協程的掛起好神奇好難懂?今天我把它的皮給扒了](https://kaixue.io/kotlin-coroutines-2/) ## 上期回顧 大部分情況下,我們都是用`launch`函數來創建協程,其實還有其他兩個函數也可以用來創建協程: * `runBlocking` * `async` `runBlocking`通常適用于單元測試的場景,而業務開發中不會用到這個函數,因為它是線程阻塞的。 接下來我們主要來對比`launch`與`async`這兩個函數。 * 相同點:它們都可以用來啟動一個協程,返回的都是`Coroutine`,我們這里不需要糾結具體是返回哪個類。 * 不同點:`async`返回的`Coroutine`多實現了`Deferred`接口。 關于`Deferred`更深入的知識就不在這里過多闡述,它的意思就是延遲,也就是結果稍后才能拿到。 我們調用`Deferred.await()`就可以得到結果了。 接下來我們繼續看看`async`是如何使用的,先回憶一下上期中獲取頭像的場景: ~~~kotlin coroutineScope.launch(Dispatchers.Main) { // ?? async 函數啟動新的協程 val avatar: Deferred = async { api.getAvatar(user) } // 獲取用戶頭像 val logo: Deferred = async { api.getCompanyLogo(user) } // 獲取用戶所在公司的 logo // ?? ?? 獲取返回值 show(avatar.await(), logo.await()) // 更新 UI } ~~~ 可以看到 avatar 和 logo 的類型可以聲明為`Deferred`,通過`await`獲取結果并且更新到 UI 上顯示。 `await`函數簽名如下: ~~~kotlin public suspend fun await(): T ~~~ 前面有個關鍵字是之前沒有見過的 ——`suspend`,這個關鍵字就對應了上期最后我們留下的一個問號:協程最核心的那個「非阻塞式」的「掛起」到底是怎么回事? 所以接下來,我們的核心內容就是來好好說一說這個「掛起」。 ## 「掛起」的本質 協程中「掛起」的對象到底是什么?掛起線程,還是掛起函數?都不對,**我們掛起的對象是協程。** 還記得協程是什么嗎?**啟動一個協程可以使用`launch`或者`async`函數,協程其實就是這兩個函數中閉包的代碼塊。** `launch`,`async`或者其他函數創建的**協程,在執行到某一個`suspend`函數的時候,這個協程會被「suspend」,也就是被掛起。** **那此時又是從哪里掛起?從當前線程掛起。換句話說,就是這個協程從正在執行它的線程上脫離。** >[success]注意,**不是這個協程停下來了!是脫離,當前線程(協程所在的線程)從這行代碼開始不再運行這個協程了,不再管這個協程要去做什么了**。 suspend 是有暫停的意思,但我們在協程中應該理解為:**當線程執行到協程的 suspend 函數的時候,暫時不繼續執行協程代碼了**。 我們先讓時間靜止,然后兵分兩路,分別看看這兩個互相脫離的線程和協程接下來將會發生什么事情: ### **線程:** 前面我們提到,**掛起會讓協程從正在執行它的線程上脫離**,具體到代碼其實是: **協程的代碼塊中,線程執行到了 suspend 函數這里的時候,就暫時不再執行剩余的協程代碼,跳出協程的代碼塊。** 那線程接下來會做什么呢?該干嘛干嘛 如果它是一個后臺線程: * 要么無事可做,被系統回收 * 要么繼續執行別的后臺任務 **總之,跟 Java 線程池里的線程在工作結束之后是完全一樣的:回收或者再利用。** **如果這個線程它是 Android 的主線程,那它接下來就會繼續回去工作:也就是一秒鐘 60 次的界面刷新任務**。 什么是繼續回去工作?示例如下 ~~~kotlin // 主線程中 GlobalScope.launch(Dispatchers.Main) { val image = suspendingGetImage(imageId) // 獲取圖片 avatarIv.setImageBitmap(image) // 顯示出來 } suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) { ... } //相當于: handler.post { val image = suspendingGetImage(imageId) avatarIv.setImageBitmap(image) } ~~~ 首先,如果你啟動一個執行主線程任務的協程,它實質上會往你的主線程post()一個新任務Runnable,這個任務Runnable就是你的協程代碼需要完成的任務,那么當這個協程被掛起的時候,那實質上就是你post()的這個任務Runnable提前結束了。那這時候主線程干嘛呢?繼續刷新界面唄。那剩下的代碼怎么辦?協程不是還沒執行完么?剛才也說了,兵分兩路。稍后看協程。 一個常見的場景是,獲取一個圖片,然后顯示出來: ~~~kotlin // 主線程中 GlobalScope.launch(Dispatchers.Main) { val image = suspendingGetImage(imageId) // 獲取圖片 avatarIv.setImageBitmap(image) // 顯示出來 } suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) { ... } ~~~ 這段執行在主線程的協程,它實質上會往你的主線程`post`一個`Runnable`,這個`Runnable`就是你的協程代碼: ~~~kotlin handler.post { val image = suspendingGetImage(imageId) avatarIv.setImageBitmap(image) } ~~~ 當這個協程被掛起的時候,就是主線程`post`的這個`Runnable`提前結束,然后繼續執行它界面刷新的任務。 關于線程,我們就看完了。 這個時候你可能會有一個疑問,那`launch`包裹的剩下代碼怎么辦?協程不是還沒執行完么?剛才也說了,兵分兩路。稍后看協程。 所以接下來,我們來看看協程這一邊。 ### **協程:** ***** 線程的代碼在到達`suspend`函數的時候被掐斷,接下來協程會從這個`suspend`函數開始繼續往下執行,不過是在**指定的線程**。 ***** **誰指定的?是`suspend`函數指定的,比如我們這個例子中,函數內部的`withContext`傳入的`Dispatchers.IO`所指定的 IO 線程。** 另外在掛起函數執行完成之后,協程為我們做的最爽的事就來了,**它會自動幫我們把協程再切回來**。 #### **`Dispatchers`調度器小知識** ***** `Dispatchers`調度器,它可以將協程限制在一個特定的線程執行,或者將它分派到一個線程池,或者讓它不受限制地運行,關于`Dispatchers`這里先不展開了。 那我們平日里常用到的調度器有哪些? 常用的`Dispatchers`,有以下三種: * `Dispatchers.Main`:Android 中的主線程 * `Dispatchers.IO`:針對磁盤和網絡 IO 進行了優化,適合 IO 密集型的任務,比如:讀寫文件,操作數據庫以及網絡請求 * `Dispatchers.Default`:適合 CPU 密集型的任務,比如計算 ***** 回到我們的協程,它從`suspend`函數開始脫離啟動它的線程,繼續執行在`Dispatchers`所指定的 IO 線程。 緊接著在`suspend`函數執行完成之后,協程為我們做的最爽的事就來了:會**自動幫我們把線程再切回來**。 這個「切回來」是什么意思? 我們的協程原本是運行在**主線程**的,當代碼遇到 suspend 函數的時候,發生線程切換,根據`Dispatchers`切換到了 IO 線程;這個所謂的切回來就是:當這個掛起函數執行完畢后,協程會幫我再`post`一個`Runnable`任務,讓我剩下的代碼繼續回到主線程去執行。這就是為啥你指定線程的那個參數不叫`Threads`,而是叫做`Dispatchers`調度器,它不只是只能指定協程執行的線程,還能在suspend掛起函數之后自動再切回來。其實,也不是一定會切回來,也可以通過設置特殊的`Dispatchers`來讓掛起函數執行完之后也不切回來,不過這是你自己的選擇,而不是它的定位。**掛起的定位就是暫時切走,稍后再切回來**。 我們從線程和協程的兩個角度都分析完成后,終于可以對協程的「掛起」suspend 做一個解釋: **協程在執行到有 suspend 標記的函數的時候,會被 suspend 也就是被掛起,而所謂的被掛起,其實個開啟一個協程一樣,說起來比較玄乎,但其實就是切個線程;** 不過區別在于,**掛起函數在執行完成之后,協程會重新切回它原先的線程**。 再簡單來講,在 Kotlin 中所謂的掛起,其實就是**一個稍后會被自動切回來的線程調度操作**。 >[success] 這個「切回來」的動作,在 Kotlin 里叫做[resume](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/-continuation/resume.html),恢復。 那么上期我們最后一個問題,**為什么掛起函數只能在協程里或者另一個掛起函數里面被調用?** * 首先,通過剛才的分析我們知道:**掛起之后是需要恢復**。而**恢復這個功能是協程的**,如果你不在協程里面調用,恢復這個功能沒法實現。 * 另外,再細想下這個邏輯:一個掛起函數要么在協程里被調用,要么在另一個掛起函數里被調用,那么它其實就是直接或者間接地,總是會在一個協程里被調用的。 所以,**要求`suspend`函數只能在協程里或者另一個 suspend 函數里被調用,還是為了要讓協程能夠在`suspend`函數切換線程之后再切回來**。 ## 怎么就「掛起」了? 我們**先了解到了什么是「掛起」后,再接著看看這個「掛起」是怎么做到的**。 首先你可以寫一個自定義的`suspend`函數,然后在主線程上的協程里去調用它,你會發現它還是運行在主線程,沒有切換。 ~~~kotlin suspend fun suspendingPrint(Dispatchers.Main) { println("Thread: ${Thread.currentThread().name}") } launch(){ suspendingPrint() } I/System.out: Thread: main ~~~ 輸出的結果還是在主線程。沒有切換。 為什么沒切換線程?因為它不知道往哪切,需要我們告訴它。 對比之前例子中`suspendingGetImage`函數代碼: ~~~kotlin // ?? suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) { ... } ~~~ 我們可以發現不同之處其實在于`withContext`函數。 其實通過`withContext`源碼可以知道,它本身就是一個掛起函數,它接收一個`Dispatcher`參數,依賴這個`Dispatcher`參數的指示,你的協程被掛起,然后切到別的線程。 所以這個`suspend`,其實并不是起到把任何把協程掛起,或者說切換線程的作用。還需要你在掛起函數里面去調用另外一個掛起函數,而且里面這個掛起函數需要是協程自帶的、內部實現了協程掛起代碼的,或者它不是自帶的,但它的內部直接或者間接地調用了某一個自帶的掛起函數,這也是可以的,總之你最終需要調用到一個自帶的掛起函數,讓它來去真正做掛起,也就是線程切換的工作。 **真正掛起協程這件事,是 Kotlin 的協程框架幫我們做的**。也就是說**所謂的協程被掛起或者說切線程這件事,它并不是發生在你外部這個掛起函數被調用的時候,而是里面那個掛起函數,那個`withContext`函數被調用的時候**。 所以我們**想要自己寫一個掛起函數,僅僅只加上`suspend`關鍵字是不行的,還需要函數內部直接或間接地調用到 Kotlin 協程框架自帶的`suspend`函數(掛起)才行**。 >[info]備注:自帶的掛起函數不只是`withContext()`一個,還有其他的,他們都能實現協程的掛起,而我們要想自己寫一個自定義的掛起函數,就需要在這個自定義的掛起函數內部直接或者間接地去調用到某一個自帶的掛起函數才行。 ## suspend 的意義? 這個`suspend`關鍵字,**既然它并不是真正實現掛起,那它的作用是什么?** **它其實是一個提醒。** 誰對誰的提醒? **函數的創建者對函數的調用者的提醒:我是一個耗時函數,我被我的創建者用掛起的方式放在后臺運行,所以請在協程里調用我。表面上它是一個要求,你需要在協程里調用我,但本質上,它其實是一個提醒——我是一個被自動放在后臺運行的耗時函數,所以你需要在協程里調用我** 這個提醒又有什么作用呢?**它能讓我們的主線程不卡**,對比我們在寫Java代碼時,在主線程做事需要非常小心,一不留神,我們在主線程調用了一個耗時方法,那就會卡一下,而且這種事情是很難避免的。我又不知道哪個方法會耗時?又不是我寫的,就算是我寫的,萬一哪天給忘了呢?而**協程通過掛起函數這種方式,它把耗時任務切線程這個工作,實際上交給了函數的創建者,而不是調用者。對于調用者而言,事情非常簡單,它只會收到一個提醒,你需要把我放在協程里面,剩下的其他調用者都不用管,而通過`suspend`關鍵字這種方式,它實際上作為一個提醒,是形成了一種機制,一種讓所有耗時任務全都自動放在后臺執行的機制,那么主線程是不是就不卡了,所以為什么`suspend`關鍵字并沒有實際去操作掛起,但 Kotlin 卻把它提供出來讓我們使用?因為它本來就不是用來操作掛起的**。 **掛起的操作 —— 也就是切線程,依賴的是掛起函數里面的實際代碼,而不是這個關鍵字**。 所以這個關鍵字,**只是一個提醒**。 還記得剛才我們嘗試自定義掛起函數的方法嗎? ~~~kotlin // ?? redundant suspend modifier suspend fun suspendingPrint() { println("Thread: ${Thread.currentThread().name}") } ~~~ ![](https://img.kancloud.cn/e1/be/e1be27eb1139c204f12a7d1d0a3a1801_865x91.png) 如果你創建一個`suspend`函數但它內部不包含真正的掛起邏輯,編譯器會給你一個提醒:`redundant suspend modifier`,告訴你這個`suspend`是多余的。 因為你這個函數實質上并沒有發生掛起,那你這個`suspend`關鍵字只有一個效果:就是限制這個函數只能在協程里被調用,如果在非協程的代碼中調用,就會編譯不通過。 所以,**創建一個`suspend`函數,為了讓它包含真正掛起的邏輯,要在它內部直接或間接調用 Kotlin 自帶的`suspend`函數,你的這個`suspend`才是有意義的**。 ## 怎么自定義 suspend 函數? 在了解了`suspend`關鍵字的來龍去脈之后,我們就可以進入下一個話題了:怎么自定義`suspend`函數。 這個「怎么自定義」其實分為兩個問題: * 什么時候需要自定義`suspend`函數? * 原則:耗時 * 具體該怎么寫呢? ### 什么時候需要自定義 suspend 函數 如果你的某個函數比較耗時,也就是要等的操作,那就把它寫成`suspend`函數。這就是原則。 **耗時操作一般分為兩類:I/O 操作和 CPU 計算工作。比如文件的讀寫、網絡交互、圖片的模糊處理,都是耗時的,通通可以把它們寫進`suspend`函數里。** **另外這個「耗時」還有一種特殊情況,就是這件事本身做起來并不慢,但它需要等待,比如 5 秒鐘之后再做這個操作。這種也是`suspend`函數的應用場景**。 ### 具體該怎么寫 給函數加上`suspend`關鍵字,然后在`withContext`把函數的內容包住就可以了。 提到用`withContext`是因為它在掛起函數里功能最簡單直接:把線程自動切走和切回。 當然并不是只有`withContext`這一個函數來輔助我們實現自定義的`suspend`函數,別的掛起函數功能總會比它多一些或者少一些,比如還有一個掛起函數叫`delay`,它的作用是等待一段時間后再繼續往下執行代碼。 使用它就可以實現剛才提到的等待類型的耗時操作: ~~~kotlin suspend fun suspendUntilDone() { while (!done) { delay(5) } } ~~~ 這些東西,在我們初步使用協程的時候不用立馬接觸,可以先把協程最基本的方法和概念理清楚。 ## 總結 我們今天整個文章其實就在理清一個概念:什么是掛起?**掛起,就是一個稍后會被自動切回來的線程調度操作。** 好,關于協程中的「掛起」我們就解釋到這里。 可能你心中還會存在一些疑惑: * 協程中掛起的「非阻塞式」到底是怎么回事? * 協程和 RxJava 在切換線程方面功能是一樣的,都能讓你寫出避免嵌套回調的復雜并發代碼,那協程還有哪些優勢,或者讓開發者使用協程的理由? 這些疑惑的答案,我們都會在下一篇中全部揭曉。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看