Android Kotlin 協程初探

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/10/24/17784848.html
-Advertisement-
Play Games

1 它是什麼(協程 和 Kotlin協程) 1.1 協程是什麼 維基百科:協程,英文Coroutine [kəru’tin] (可入廳),是電腦程式的一類組件,推廣了協作式多任務的子程式,允許執行被掛起與被恢復。 作為Google欽定的Android開發首選語言Kotlin,協程並不是 Kotli ...


1 它是什麼(協程 和 Kotlin協程)

1.1 協程是什麼

維基百科:協程,英文Coroutine [kəru’tin] (可入廳),是電腦程式的一類組件,推廣了協作式多任務的子程式,允許執行被掛起與被恢復。

作為Google欽定的Android開發首選語言Kotlin,協程並不是 Kotlin 提出來的新概念,目前有協程概念的編程語言有Lua語言、Python語言、Go語言、C語言等,它只是一種編程思想,不局限於特定的語言。

而每一種編程語言中的協程的概念及實現又不完全一樣,本次分享主要講Kotlin協程。

1.2 Kotlin協程是什麼

Kotlin官網:協程是輕量級線程

可簡單理解:一個線程框架,是全新的處理併發的方式,也是Android上方便簡化非同步執行代碼的方式

類似於 Java:線程池 Android:Handler和AsyncTask,RxJava的Schedulers

註:Kotlin不僅僅是面向JVM平臺的,還有JS/Native,如果用kotlin來寫前端,那Koltin的協程就是JS意義上的協程。如果僅僅JVM 平臺,那確實應該是線程框架。

1.3 進程、線程、協程比較

可通過以下兩張圖理解三者的不同和關係

2 為什麼選擇它(協程解決什麼問題)

非同步場景舉例:

  1. 第一步:介面獲取當前用戶token及用戶信息
  2. 第二步:將用戶的昵稱展示界面上
  3. 第三步:然後再通過這個token獲取當前用戶的消息未讀數
  4. 第四步:並展示在界面上

2.1 現有方案實現

apiService.getUserInfo().enqueue(object :Callback<User>{
    override fun onResponse(call: Call<User>, response: Response<User>) {
        val user = response.body()
        tvNickName.text = user?.nickName
        apiService.getUnReadMsgCount(user?.token).enqueue(object :Callback<Int>{
            override fun onResponse(call: Call<Int>, response: Response<Int>) {
                val tvUnReadMsgCount = response.body()
                tvMsgCount.text = tvUnReadMsgCount.toString()
            }
        })
    }
})

現有方案如何拿到非同步任務的數據,得不到就毀掉哈哈哈,就是通過回調函數來解決。
若嵌套多了,這種畫風是不是有點回調地獄的感覺,俗稱的「callback hell」

2.2 協程實現

mainScope.launch {
    val user = apiService.getUserInfoSuspend() //IO線程請求數據
    tvNickName.text = user?.nickName //UI線程更新界面
    val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //IO線程請求數據
    tvMsgCount.text = unReadMsgCount.toString() //UI線程更新界面
}
suspend fun getUserInfoSuspend() :User? {
    return withContext(Dispatchers.IO){
        //模擬網路請求耗時操作
        delay(10)
        User("asd123", "userName", "nickName")
    }
}

suspend fun getUnReadMsgCountSuspend(token:String?) :Int{
    return withContext(Dispatchers.IO){
        //模擬網路請求耗時操作
        delay(10)
        10
    }
}

紅色框框內的就是一個協程代碼塊。

可以看得出在協程實現中告別了callback,所以再也不會出現回調地獄這種情況了,協程解決了回調地獄

協程可以讓我們用同步的代碼寫出非同步的效果,這也是協程最大的優勢,非同步代碼同步去寫。

小結:協程可以非同步代碼同步去寫,解決回調地獄,讓程式員更方便地處理非同步業務,更方便地切線程,保證主線程安全。

它是怎麼做到的?

3 它是怎麼工作的(協程的原理淺析)

3.1 協程的掛起和恢復

掛起(非阻塞式掛起)

suspend 關鍵字,它是協程中核心的關鍵字,是掛起的標識。

下麵看一下上述示例代碼切換線程的過程:

每一次從主線程切到IO線程都是一次協程的掛起操作;

每一次從IO線程切換主線程都是一次協程的恢復操作;

掛起和恢復是suspend函數特有的能力,其他函數不具備,掛起的內容是協程,不是掛起線程,也不是掛起函數,當線程執行到suspend函數的地方,不會繼續執行當前協程的代碼了,所以它不會阻塞線程,是非阻塞式掛起。

有掛起必然有恢復流程, 恢復是指將已經被掛起的目標協程從掛起之處開始恢復執行。在協程中,掛起和恢復都不需要我們手動處理,這些都是kotlin協程幫我們自動完成的。

那Kotlin協程是如何幫我們自動實現掛起和恢復操作的呢?

它是通過Continuation來實現的。 [kənˌtɪnjuˈeɪʃ(ə)n] (繼續;延續;連續性;後續部分)

3.2 協程的掛起和恢復的工作原理(Continuation)

CPS + 狀態機

Java中沒有suspend函數,suspend是Kotlin中特有的關鍵字,當編譯時,Kotlin編譯器會將含有suspend關鍵字的函數進行一次轉換。

這種被編譯器轉換在kotlin中叫CPS轉換(cotinuation-passing-style)。

轉換流程如下所示

程式員寫的掛起函數代碼:

suspend fun getUserInfo() : User {
    val user = User("asd123", "userName", "nickName")
    return user
}

假想的一種中間態代碼(便於理解):

fun getUserInfo(callback: Callback<User>): Any? {
    val user = User("asd123", "userName", "nickName")
    callback.onSuccess(user)
    return Unit
}

轉換後的代碼:

fun getUserInfo(cont: Continuation<User>): Any? {
    val user = User("asd123", "userName", "nickName")
    cont.resume(user)
    return Unit
}

我們通過Kotlin生成位元組碼工具查看位元組碼,然後將其反編譯成Java代碼:

@Nullable
public final Object getUserInfo(@NotNull Continuation $completion) {
   User user = new User("asd123", "userName", "nickName");
   return user;
}

這也驗證了確實是會通過引入一個Continuation對象來實現恢復的流程,這裡的這個Continuation對象中包含了Callback的形態

它有兩個作用:1. 暫停並記住執行點位;2. 記住函數暫停時刻的局部變數上下文。

所以為什麼我們可以用同步的方式寫非同步代碼,是因為Continuation幫我們做了回調的流程。

下麵看一下這個Continuation 的源碼部分

可以看到這個Continuation中封裝了一個resumeWith的方法,這個方法就是恢復用的。

internal abstract class BaseContinuationImpl() : Continuation<Any?> {


    public final override fun resumeWith(result: Result<Any?>) {
        //省略好多代碼
        invokeSuspend()
        //省略好多代碼
    }


    protected abstract fun invokeSuspend(result: Result<Any?>): Any?
}


internal abstract class ContinuationImpl(
    completion: Continuation<Any?>?,
    private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {

protected abstract fun invokeSuspend(result: Result<Any?>): Any?

//invokeSuspend() 這個方法是恢復的關鍵一步

繼續看上述例子:

這是一個CPS之前的代碼:

suspend fun testCoroutine() {
    val user = apiService.getUserInfoSuspend() //掛起函數  IO線程
    tvNickName.text = user?.nickName //UI線程更新界面
    val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //掛起函數  IO線程
    tvMsgCount.text = unReadMsgCount.toString() //UI線程更新界面
}

當前掛起函數里有兩個掛起函數

通過kotlin編譯器編譯後:

fun testCoroutine(completion: Continuation<Any?>): Any? {
    // TestContinuation本質上是匿名內部類
    class TestContinuation(completion: Continuation<Any?>?) : ContinuationImpl(completion) {
        // 表示協程狀態機當前的狀態
        var label: Int = 0


        // 兩個變數,對應原函數的2個變數
        lateinit var user: Any
        lateinit var unReadMsgCount: Int


        // result 接收協程的運行結果
        var result = continuation.result


        // suspendReturn 接收掛起函數的返回值
        var suspendReturn: Any? = null


        // CoroutineSingletons 是個枚舉類
        // COROUTINE_SUSPENDED 代表當前函數被掛起了
        val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED


        // invokeSuspend 是協程的關鍵
        // 它最終會調用 testCoroutine(this) 開啟協程狀態機
        // 狀態機相關代碼就是後面的 when 語句
        // 協程的本質,可以說就是 CPS + 狀態機
        override fun invokeSuspend(_result: Result<Any?>): Any? {
            result = _result
            label = label or Int.Companion.MIN_VALUE
            return testCoroutine(this)
        }
    }


    // ...
    val continuation = if (completion is TestContinuation) {
        completion
    } else {
        //                作為參數
        //                   ↓
        TestContinuation(completion)
loop = true
while(loop) {
when (continuation.label) {
    0 -> {
        // 檢測異常
        throwOnFailure(result)


        // 將 label 置為 1,準備進入下一次狀態
        continuation.label = 1


        // 執行 getUserInfoSuspend(第一個掛起函數)
        suspendReturn = getUserInfoSuspend(continuation)


        // 判斷是否掛起
        if (suspendReturn == sFlag) {
            return suspendReturn
        } else {
            result = suspendReturn
            //go to next state
        }
    }


    1 -> {
        throwOnFailure(result)


        // 獲取 user 值
        user = result as Any


        // 準備進入下一個狀態
        continuation.label = 2


        // 執行 getUnReadMsgCountSuspend
        suspendReturn = getUnReadMsgCountSuspend(user.token, continuation)


        // 判斷是否掛起
        if (suspendReturn == sFlag) {
            return suspendReturn
        } else {
            result = suspendReturn
            //go to next state
        }
    }


    2 -> {
        throwOnFailure(result)


        user = continuation.mUser as Any
        unReadMsgCount = continuation.unReadMsgCount as Int
        loop = false
}
}

通過一個label標簽控制分支代碼執行,label為0,首先會進入第一個分支,首先將label設置為下一個分支的數值,然後執行第一個suspend方法並傳遞當前Continuation,得到返回值,如果是COROUTINE SUSPENDED,協程框架就直接return,協程掛起,當第一個suspend方法執行完成,會回調Continuation的invokeSuspend方法,進入第二個分支執行,以此類推執行完所有suspend方法。

每一個掛起點和初始掛起點對應的 Continuation 都會轉化為一種狀態,協程恢復只是跳轉到下一種狀態中。掛起函數將執行過程分為多個 Continuation 片段,並且利用狀態機的方式保證各個片段是順序執行的。

小結:協程的掛起和恢復的本質是CPS + 狀態機

4 總結

總結幾個不用協程實現起來很麻煩的騷操作:

  1. 如果有一個函數,它的返回值需要等到多個耗時的非同步任務都執行完畢返回之後,組合所有任務的返回值作為 最終返回值
  2. 如果有一個函數,需要順序執行多個網路請求,並且後一個請求依賴前一個請求的執行結果
  3. 當前正在執行一項非同步任務,但是你突然不想要它執行了,隨時可以取消
  4. 如果你想讓一個任務最多執行3秒,超過3秒則自動取消

Kotlin協程之所以被認為是假協程,是因為它並不在同一個線程運行,而是真的會創建多個線程。

Kotlin協程在Android上只是一個類似線程池的封裝,真就是一個線程框架。但是它卻可以讓我們用同步的代碼風格寫出非同步的效果,至於怎麼做的,這個不需要我們操心,這些都是kotlin幫我們處理好了,我們需要關心的是怎麼用好它

它就是一個線程框架。

作者:京東物流 王斌

來源:京東雲開發者社區 自猿其說Tech 轉載請註明來源


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • atexit 處理器中再次調用 exit 為什麼能正常運行?atexit 處理器中再次調用 atexit 註冊的函數為什麼能正常被調用?帶著這些疑問來看看 glibc 是用什麼數據結構存儲終止處理器的,另外看看列印這些結構時遇到了哪些問題 ...
  • 1. 如何構建資料庫環境 1.1. 托管MySQL 1.2. VM上構建 1.3. 天下沒有免費的午餐,每一個選擇都伴隨著一系列的權衡 2. 托管MySQL 2.1. 服務商提供了一個可訪問的資料庫設置程式,而不需要用戶深入瞭解MySQL的具體細節 2.2. 使用托管MySQL將缺乏很多的可見性和控 ...
  • 緩存驅逐是指從緩存中刪除特定數據的過程。當緩存達到最大存儲容量時,必須刪除一些數據,為新數據騰出空間。本文將深入探討與緩存驅逐有關的細節,並就如何選擇合適的緩存驅逐策略給出建議。 ...
  • 如今,大規模、高時效、智能化數據處理已是“剛需”,企業需要更強大的數據平臺,來應對數據查詢、數據處理、數據挖掘、數據展示以及多種計算模型並行的挑戰,湖倉一體方案應運而生。 《實時湖倉實踐五講》是袋鼠雲打造的系列直播活動,將圍繞實時湖倉的建設趨勢和通用問題,邀請奮戰於企業數字化一線的核心產品&技術專家 ...
  • 通過實時索引、查詢和全文搜索引擎,Redis Enterprise提供了更好的數據檢索解決方案。通過強大的搜索引擎助力,Redis Enterprise能在亞毫秒級的時間內提供結果,以增強客戶體驗並助力商業智能。 ...
  • 本文分享自華為雲社區《GaussDB(DWS)性能調優:實時場景下表行數估算不准確引起的的性能瓶頸問題案例》,作者: O泡果奶~。 本文針對實時場景下SQL語句因表行數估算不准確而導致語句執行超時報錯的案例進行分析。 1、【問題描述】 實時場景下,select查詢語句執行時間過長,該語句verbos ...
  • 數據泄露對企業的影響是嚴重的,包括商業機密泄露、法律責任和信譽喪失。為了降低數據泄露的風險,NineData推出了SQL開發規範和用戶訪問量管理功能。用戶訪問量管理功能可以根據用戶的職責和工作需求,靈活配置訪問量,並對特定用戶單獨配置訪問量,並設置到期時間。這個功能適用於數據安全、法律合規和應急響應... ...
  • 1、華為官網介紹 2、OpenHarmony開源項目 3、技術架構 內核層 內核子系統:採用多內核(Linux內核或者LiteOS)設計,支持針對不同資源受限設備選用適合的OS內核 驅動子系統:驅動框架(HDF)是系統硬體生態開放的基礎,提供統一外設訪問能力和驅動開發、管理框架。 系統服務層 系統服 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...