餓了麼資深Android工程師帶你領略Kotlin協程的力量

餓了麼資深Android工程師帶你領略Kotlin協程的力量

內容來源:2018 年 6 月 28 日,餓了麼資深Android工程師張濤在“droidcon上海2018安卓技術大會”進行《領略kotlin協程的力量》演講分享。IT 大咖說作為獨家視頻合作方,經主辦方和講者審閱授權發佈。

閱讀字數:3232 | 9分鐘閱讀

嘉賓演講視頻及PPT,請複製:http://t.cn/RkfTgMe,粘貼至瀏覽器即可。

餓了麼資深Android工程師帶你領略Kotlin協程的力量

摘要

協程相對多線程有著更易於控制的優勢,很多語言都提供了協程的能力,kotlin也不例外。本次分享將通過一個小案例展示協程在kotlin中是如何應用的,以及如何在現有項目中引入協程。

協程是什麼

進程

早期的計算機運行程序還是隻能一次運行一個任務,之後進程的出現實現了近似同步的執行效果,其本質上是程序的交替執行。為了保證進程中的程序能夠正常執行,還會有一些存儲進程狀態的保存集。隨著硬件的發展和多CPU的出現,能夠同時執行的進程數量逐漸增多。這就帶來了一個問題,即用來存儲進程狀態的集合所佔用的資源比一個進程可以執行的資源還要多,相當於整個系統大半的進行都是用來保存進程的狀態。

線程

線程的提出有效的解決了這個問題。進程不再頻繁的切換,而是先執行,遇到阻塞的話暫時不管,繼續執行其他的任務,當其他任務執行完之後再回過頭來看阻塞任務是否執行完。多線程的缺陷在於無法自主控制調度,除開一定會執行的主線程之外,其他線程的執行順序都無法控制,在Java上是由Java虛擬機調度,其他平臺大多是由系統控制。

協程

線程執行過程中發生線程切換的時候會損耗一定的資源,這部分資源用來保存線程的狀態。執行過程中如果發生了磁盤讀寫或網絡請求這樣的IO操作的時候線程的執行會被阻塞,但同時該線程還會持有CPU資源,這就造成了一定了資源浪費。理想的情況是在發送阻塞的時候,該線程主動交出CPU給其他線程使用或者給內部的其他任務。

這種方式其實就是協程的體系。通過提升CPU利用率,減少線程切換,進而提升程序運行效率。

延伸開來協程主要有三個特性。第一個是可控制,不同於線程協程能做到可被控制的發起子任務;第二個是輕量級,協程非常小、佔用資源比線程還少,在JVM平臺上它的本質就是一次方法的調用;第三個是語法糖,目前能夠使用協程的語言都提供了很好的語法糖支持,使多任務或多線程切換不在使用回調語法。

通過Kotlin在JVM平臺使用協程

示例:第三方登錄

第三登錄在應用開發中可以算是一個很常見的場景,具體的邏輯是這樣的,首先向第三方平臺請求用戶token,然後將token和自身平臺上的用戶賬號關聯起來,最後獲取用戶信息展示到UI界面上。

餓了麼資深Android工程師帶你領略Kotlin協程的力量

對此最常見的做法是採用回調的形式。requestToken會先發出一次網絡請求,請求返回後執行回調並傳入token,回調內部又會用token作為參數向我們自己的服務器發起請求獲得到用戶信息,最終完成用戶信息在UI上的改變。這種方式的問題在於嵌套層級過多,鏈路一旦過長看起來會非常複雜。

協程改寫

餓了麼資深Android工程師帶你領略Kotlin協程的力量

要改變這種現狀,自然就要用到協程,上圖是用協程對前面示例的改寫。在Kotlin中如果函數的函數體內只有一個語句,那麼就可以將這條語句直接賦值給函數聲明。另外如果方法只有一個參數且該參數為lambda表達式的時候,可以將函數後小括號省略掉。

在Kotlin中常用的啟動協程的方式有三種。第一種是上圖中的runBlocking,它只會用在協程和線程的交接點,也就是通常只用於啟動最外層協程。第二種是launch,用於在協程內部再啟動一個協程。第三種是async/await,它不僅可以啟動協程,還可以得到執行的結果。

餓了麼資深Android工程師帶你領略Kotlin協程的力量

這是前面示例中細分的兩個函數調用。因為前兩個方式都是耗時操作,所以要放在子線程中運行。但是在安卓中子線程無法做UI改變的操作,因此改變UI的時候還是要切換到主線程。setText方法的launch中有一個UI參數,這是Kotlin的協程提供的對象,表示在UI線程中啟動協程,同時協程被中斷以後的恢復也是在UI線程中。

在requestToken函數內部中的return@async標識用來表示返回的是async這個閉包的內部邏輯。Async的CommonPool參數是Kotlin提供的線程池的單例對象,有了這個參數後就可以從線程池中隨機的取一個子線程,然後運行閉包中的邏輯。當網絡請求操作執行完之後,await函數會將請求結果直接返回給requestToken。

協程的本質

一般直接將一個耗時方法寫入在代碼中其實是有問題的,輕則會UI卡頓,嚴重的話還會造成程序無響應。因此Kotlin協程庫提供了一個關鍵字suspend,表示掛起指出該方法是一個協程方法不是直接運行在UI線程中。Suspend修飾的函數(或lambda)只能被suspend修飾的函數(或lambda)調用。

餓了麼資深Android工程師帶你領略Kotlin協程的力量

圖中被suspend修飾的requestToken函數在被編譯之後會變成下方這種形式。這種語法其實在Java中經常會用到,它返回一個Object,不過多另一個Continuation類型的泛型參數,表示協程方法requestToken最終會返回一個string的值。

餓了麼資深Android工程師帶你領略Kotlin協程的力量

Continuation是協程在代碼上的映射,它本質上是個接口,Kotlin中每個協程的協程體都實現了這個接口。仔細看下該接口內部的代碼就會發現這就是一個回調接口。協程的本質也就是一次回調,只不過通過語法糖的形式讓它看起來像是順序執行。

協程切換

餓了麼資深Android工程師帶你領略Kotlin協程的力量

上圖中每個大括號所包含的範圍是協程的執行過程。發生協程切換的時候會有一個suspend Point點,通常被稱作協程的掛起點,比如從協程0到協程1發生一次協程切換的時候,協程0會被掛起,協程1得以執行。

餓了麼資深Android工程師帶你領略Kotlin協程的力量

前面第三方登錄的協程方法在被編譯完之後會在代碼中出現上圖所示的方法doResume。它有一個any參數,該參數類似於Java中Object。Kotlin中所有類都會有一個直接或間接的父類指向any,這裡的any其實就是協程對象。

當前類繼承自CoroutineImpl,CoroutineImpl是Continuation的實現類。方法內部的this指向doResume傳入的參數,第一個協程啟動的時候this.label默認為0,requestToken方法被調用同時label值被改為1,requestToken執行完之後會通過傳入的this參數繼續調用doResume方法。這時的label值已經變為了1,所以會執行協程的第二段操作,通過這樣的一系列執行就完成了整個協程的切換。

方案:SPP+PHP

餓了麼資深Android工程師帶你領略Kotlin協程的力量

Kotlin提供了一個協程擴展庫,可以直接返回Call類型的對象。這個對象的擴展形式就是圖中展示的,可以看到它被靜態的添加了個await擴展方法,這個await就是一個協程方法和之前所提到的await並無差別。

餓了麼資深Android工程師帶你領略Kotlin協程的力量

上圖的代碼中當網絡請求被執行完之後會得到一個Call對象,通過調用它的await方法就能夠獲取到請求的返回值。

餓了麼資深Android工程師帶你領略Kotlin協程的力量

這是擴展方法的具體實現,整個函數只有一個函數體,內部啟動了一個協程。Enqueue表示將請求加入到請求隊列中,請求成功後會通過異步回調拿到執行結果。這裡回調的時候又進一步調用了協程接口continuation的resume方法和resumeWithException方法。拿到這兩個回調方法之後,編譯器在編譯的時候會直接在對應的位置觸發接下來的代碼。

以上為今天的分享內容,謝謝大家!


分享到:


相關文章: