五分鐘技術(shù)趣談 | 試論Android異步框架Kotlin協(xié)程
Part 01
什么是協(xié)程
作為開發(fā)人員尤其是客戶端應(yīng)用開發(fā),我們一直面臨著需要解決的問題——如何防止我們的應(yīng)用程序被阻塞??紤]下面一個異步應(yīng)用場景??蛻舳隧樞蜻M行3次網(wǎng)絡(luò)請求,最后更新UI展示結(jié)果。
圖片
圖1 異步場景
有多種方法實現(xiàn)上述需求,主流的包括:
- 回調(diào)
- Rx(反應(yīng)式擴展)
- 協(xié)程
1.1 回調(diào)方式
圖2 回調(diào)代碼示例
異步回調(diào)的方式雖然實現(xiàn)了需求,但是這種結(jié)構(gòu)的代碼無論是閱讀還是維護起來都是極其糟糕的。這種回調(diào)函數(shù)的層層嵌套耦合,親切地稱為 "回調(diào)地獄"。
1.2 Rx方式
圖3 Rx代碼示例
Rx系列的鏈式調(diào)用,是在協(xié)程之前推薦的做法,RxJava豐富的操作符、簡便的線程調(diào)度、異常處理使得大多數(shù)人滿意。但是還有沒有更簡潔易讀的寫法呢?
1.3 協(xié)程方式
圖4 協(xié)程代碼示例
使用協(xié)程后的代碼非常簡潔,以順序的方式編寫異步代碼,不會阻塞當(dāng)前UI線程,錯誤處理、線程切換也和平常代碼一樣簡單。
協(xié)程具有以下幾個特點:
- 輕量:您可以在單個線程上運行多個協(xié)程,因為協(xié)程支持掛起,掛起時不需要阻塞線程,幾乎是無代價的,協(xié)程是由開發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程,非常輕量級。
- 內(nèi)存泄漏更少:使用結(jié)構(gòu)化并發(fā)機制在一個作用域內(nèi)執(zhí)行多項操作。
- 內(nèi)置取消支持:取消操作會自動在運行中的整個協(xié)程層次結(jié)構(gòu)內(nèi)傳播。
- Jetpack集成:許多Jetpack庫都提供全面協(xié)程支持的擴展。某些庫還提供自己的協(xié)程作用域,可用于結(jié)構(gòu)化并發(fā)。
總而言之:協(xié)程可以簡化異步編程,可以順序地表達程序。協(xié)程使用掛起,這意味可以在代碼的特定點暫停和恢復(fù)執(zhí)行,無需阻塞主線程或顯示創(chuàng)建額外的線程。
Part 02
協(xié)程的使用
- 引入gradle依賴
圖5 gradle依賴引入
- 啟動協(xié)程
圖6 啟動協(xié)程
上面就是啟動協(xié)程的代碼,啟動協(xié)程的代碼可以分為三部分:GlobalScope、launch、Dispatchers,它們分別對應(yīng):協(xié)程的作用域、構(gòu)建器和調(diào)度器。
2.1 協(xié)程作用域
指的是協(xié)程內(nèi)的代碼運行的時間周期范圍,如果超出了指定的協(xié)程范圍,協(xié)程會被取消執(zhí)行。
官方庫給我們提供了一些作用域可以直接來使用:
- runBlocking
頂層函數(shù),但是它會阻塞當(dāng)前線程,主要用于測試。
- GlobalScope
全局協(xié)程作用域,它啟動的協(xié)程的生命周期只受整個應(yīng)用程序的生命周期的限制,且不能取消,運行時會消耗一些內(nèi)存資源,這可能會導(dǎo)致內(nèi)存泄露,不適用于業(yè)務(wù)開發(fā)。
- coroutineScope
創(chuàng)建一個獨立的協(xié)程作用域,直到所有啟動的協(xié)程都完成后才結(jié)束自身。它是一個掛起函數(shù),需要運行在協(xié)程內(nèi)或掛起函數(shù)內(nèi),為并行分解工作而設(shè)計的。
- supervisorScope
與coroutineScope類似,不同的是子協(xié)程的異常不會影響父協(xié)程,也不會影響其他子協(xié)程。
- MainScope
為UI組件創(chuàng)建主作用域。一個頂層函數(shù),上下文是SupervisorJob() + Dispatchers.Main,說明它是一個在主線程執(zhí)行的協(xié)程作用域。推薦使用。
Android官方對協(xié)程的支持是非常友好的,KTX為Jetpack的Lifecycle相關(guān)組件提供了已經(jīng)綁定UV聲明周期的作用域供我們直接使用:
- lifecycleScope
與Lifecycle綁定生命周期,生命周期被銷毀時,此作用域?qū)⒈蝗∠粫斐蓞f(xié)程泄漏,推薦使用。
- viewModelScope
與lifecycleScope類似,與ViewModel綁定生命周期,當(dāng)ViewModel被清除時,這個作用域?qū)⒈蝗∠扑]使用。
2.2 調(diào)度器
調(diào)度器的作用是將協(xié)程限制在特定的線程執(zhí)行。主要的調(diào)度器類型有:
- Dispatchers.Main:指定執(zhí)行的線程是主線程
- Dispatchers.IO:指定執(zhí)行的線程是IO線程
- Dispatchers.Default:默認的調(diào)度器,適合執(zhí)行CPU密集性的任務(wù)
- Dispatchers.Unconfined:非限制的調(diào)度器,指定的線程可能會隨著掛起的函數(shù)的發(fā)生變化
2.3 構(gòu)建器
kotlinx.continues庫提供的三個基本協(xié)程構(gòu)建器:
- Launch
- async
- runBlocking
launch{}是最常用的協(xié)程構(gòu)建器,不阻塞當(dāng)前線程,在后臺創(chuàng)建一個新協(xié)程,也可以指定協(xié)程調(diào)度器。
async創(chuàng)建一個新的協(xié)程,不會阻塞當(dāng)前線程,必須在協(xié)程作用域中才可以調(diào)用,并返回Deffer對象。可通過調(diào)用Deffer.await()方法等待該子協(xié)程執(zhí)行完成并獲取結(jié)果。常用于并發(fā)執(zhí)行-同步等待和獲取返回值的情況。
runBlocking是創(chuàng)建一個新的協(xié)程同時阻塞當(dāng)前線程,直到協(xié)程結(jié)束,主要是為測試設(shè)計。
Part 03
協(xié)程掛起、恢復(fù)原理剖析
協(xié)程的概念最核心的點就是掛起,即函數(shù)或者某段程序可以在某個時刻暫停執(zhí)行并稍后恢復(fù)。suspend是Kotlin協(xié)程最核心的關(guān)鍵字,使用suspend關(guān)鍵字修飾的函數(shù)叫作掛起函數(shù),掛起函數(shù)只能在協(xié)程體內(nèi)或者在其他掛起函數(shù)內(nèi)調(diào)用。內(nèi)部實現(xiàn)使用了Kotlin編譯器的一些編譯技術(shù),被關(guān)鍵字suspend修飾的方法在編譯階段,編譯器會修改方法的簽名. 包括返回值,修飾符,入?yún)?方法體實現(xiàn)。我們以下面一個簡單的掛起方法來剖析。
圖7 掛起函數(shù)
通過AS的工具欄中 Tools->Kotlin->show Kotlin ByteCode,得到j(luò)ava字節(jié)碼,再點擊Decompile按鈕反編譯成java源碼:
圖8 掛起函數(shù)反編譯java源碼
上面主要步驟為:
1??函數(shù)返回值變成Object,函數(shù)入?yún)⒕幾g后增加了Continuation參數(shù)。
2??創(chuàng)建一個ContinuationImpl ,復(fù)寫invokeSuspend()方法,在這個方法里面它又調(diào)用了一次自己,并且把continuation傳遞進去。
3??在switch狀態(tài)機中,label初始值為0,第一次會進入case 0分支,delay()是一個掛起函數(shù),傳入上面的continuation參數(shù),會有一個Object類型的返回值。
4??DelayKt.delay(2000, continuation)的返回結(jié)果如果是 COROUTINE_SUSPENDED,則直接return,那么方法執(zhí)行就被結(jié)束了,方法就被掛起了。
這就是掛起的真正原理。協(xié)程的掛起本質(zhì)上是方法的掛起,而方法的掛起本質(zhì)上是return,協(xié)程的恢復(fù)本質(zhì)上方法的恢復(fù),而恢復(fù)的本質(zhì)是callback回調(diào)。
Part 04
總結(jié)
異步編程是現(xiàn)代軟件開發(fā)的重要組成部分,它允許我們創(chuàng)建響應(yīng)迅速、可擴展的應(yīng)用程序。Kotlin協(xié)程是一款輕量級、高效、易于使用的并發(fā)框架,借助Kotlin的語言優(yōu)勢,用同步的方式寫出異步的代碼,變得更加可維護和可讀,有助于改善開發(fā)體驗。在Android客戶端開發(fā)中,結(jié)合Jetpack可以更加輕松使用不阻塞UI線程同時避免內(nèi)存泄露。