破解 Kotlin 協程
這次準備從協程用戶(也就是程序員你我他啦)的角度來寫一下,希望對大家能有幫助。
2. 需求確認
在開始講解協程之前,我們需要先確認幾件事兒:
你用過線程對吧?
你寫過回調對吧?
你用過 RxJava 類似的框架嗎?
看下你的答案:
如果上面的問題的回答都是 「Yes」,那麼太好了,這篇文章非常適合你,因為你已經意識到回調有多麼可怕,並且找到了解決方案;
如果前兩個是 「Yes」,沒問題,至少你已經開始用回調了,你是協程潛在的用戶;
如果只有第一個是 「Yes」,那麼,可能你剛剛開始學習線程,那你還是先打好基礎再來吧~
3. 一個常規例子
我們通過 Retrofit 發送一個網路請求,其中介面如下:
Retrofit 初始化如下:
那麼我們請求網路時:
請求結果回來之後,我們切換線程到 UI 線程來展示結果。這類代碼大量存在於我們的邏輯當中,它有什麼問題呢?
通過 Lambda 表達式,我們讓線程切換變得不是那麼明顯,但它仍然存在,一旦開發者出現遺漏,這裡就會出現問題
回調嵌套了兩層,看上去倒也沒什麼,但真實的開發環境中邏輯一定比這個複雜的多,例如登錄失敗的重試
重複或者分散的異常處理邏輯,在請求失敗時我們調用了一次 showError,在數據讀取失敗時我們又調用了一次,真實的開發環境中可能會有更多的重複
Kotlin 本身的語法已經讓這段代碼看上去好很多了,如果用 Java 寫的話,你的直覺都會告訴你:你在寫 Bug。
如果你不是 Android 開發者,那麼你可能不知道 handler 是什麼東西,沒關係,你可以替換為 SwingUtilities.invokeLater{ ... } (Java Swing),或者 setTimeout({ ... }, 0) (Js) 等等。
4. 改造成協程
你當然可以改造成 RxJava 的風格,但 RxJava 比協程抽象多了,因為除非你熟練使用那些 operator,不然你根本不知道它在幹嘛(試想一下 retryWhen)。協程就不一樣了,畢竟編譯器加持,它可以很簡潔的表達出代碼的邏輯,不要想它背後的實現邏輯,它的運行結果就是你直覺告訴你的那樣。
對於 Retrofit,改造成協程的寫法,有兩種,分別是通過 CallAdapter 和 suspend 函數。
4.1 CallAdapter 的方式
我們先來看看 CallAdapter 的方式,這個方式的本質是讓介面的方法返回一個協程的 Job:
注意 Deferred 是 Job 的子介面。
那麼我們需要為 Retrofit 添加對 Deferred 的支持,這需要用到開源庫:
構造 Retrofit 實例時添加:
那麼這時候我們發起請求就可以這麼寫了:
說明: Dispatchers.Main 在不同的平台上的實現不同,如果在 Android 上為 HandlerDispatcher,在 Java Swing 上為 SwingDispatcher 等等。
首先我們通過 launch 啟動了一個協程,這類似於我們啟動一個線程,launch 的參數有三個,依次為協程上下文、協程啟動模式、協程體:
啟動模式不是一個很複雜的概念,不過我們暫且不管,默認直接允許調度執行。
上下文可以有很多作用,包括攜帶參數,攔截協程執行等等,多數情況下我們不需要自己去實現上下文,只需要使用現成的就好。上下文有一個重要的作用就是線程切換,Dispatchers.Main 就是一個官方提供的上下文,它可以確保 launch 啟動的協程體運行在 UI 線程當中(除非你自己在 launch 的協程體內部進行線程切換、或者啟動運行在其他有線程切換能力的上下文的協程)。
換句話說,在例子當中整個 launch 內部你看到的代碼都是運行在 UI 線程的,儘管 getUser 在執行的時候確實切換了線程,但返回結果的時候會再次切回來。這看上去有些費解,因為直覺告訴我們,getUser 返回了一個 Deferred 類型,它的 await 方法會返回一個 User 對象,意味著 await 需要等待請求結果返回才可以繼續執行,那麼 await 不會阻塞 UI 線程嗎?
答案是:不會。當然不會,不然那 Deferred 與 Future 又有什麼區別呢?這裡 await 就很可疑了,因為它實際上是一個 suspend 函數,這個函數只能在協程體或者其他 suspend 函數內部被調用,它就像是回調的語法糖一樣,它通過一個叫 Continuation 的介面的實例來返回結果:
1.3 的源碼其實並不是很直接,儘管我們可以再看下 Result 的源碼,但我不想這麼做。更容易理解的是之前版本的源碼:
相信大家一下就能明白,這其實就是個回調嘛。如果還不明白,那就對比下 Retrofit 的 Callback:
有結果正常返回的時候,Continuation 調用 resume 返回結果,否則調用 resumeWithException 來拋出異常,簡直與 Callback 一模一樣。
所以這時候你應該明白,這段代碼的執行流程本質上是一個非同步回調:
而代碼之所以可以看起來是同步的,那就是編譯器的黑魔法了,你當然也可以叫它「語法糖」。
這時候也許大家還是有問題:我並沒有看到 Continuation 啊,沒錯,這正是我們前面說的編譯器黑魔法了,在 Java 虛擬機上,await 這個方法的簽名其實並不像我們看到的那樣:
它真實的簽名其實是:
即接收一個 Continuation 實例,返回 Object 的這麼個函數,所以前面的代碼我們可以大致理解為:
而在 await 當中,大致就是:
這樣的回調大家一看就能明白。講了這麼多,請大家記住一點:從執行機制上來講,協程跟回調沒有什麼本質的區別。
4.2 suspend 函數的方式
suspend 函數是 Kotlin 編譯器對協程支持的唯一的黑魔法(表面上的,還有其他的我們後面講原理的時候再說)了,我們前面已經通過 Deferred 的 await 方法對它有了個大概的了解,我們再來看看 Retrofit 當中它還可以怎麼用。
Retrofit 當前的 release 版本是 2.5.0,還不支持 suspend 函數。因此想要嘗試下面的代碼,需要最新的 Retrofit 源碼的支持;當然,也許你看到這篇文章的時候,Retrofit 的新版本已經支持這一項特性了呢。
首先我們修改介面方法:
這種情況 Retrofit 會根據介面方法的聲明來構造 Continuation,並且在內部封裝了 Call 的非同步請求(使用 enqueue),進而得到 User 實例,具體原理後面我們有機會再介紹。使用方法如下:
它的執行流程與 Deferred.await 類似,我們就不再詳細分析了。
5. 協程到底是什麼
好,堅持讀到這裡的朋友們,你們一定是非同步代碼的「受害者」,你們肯定遇到過「回調地獄」,它讓你的代碼可讀性急劇降低;也寫過大量複雜的非同步邏輯處理、異常處理,這讓你的代碼重複邏輯增加;因為回調的存在,還得經常處理線程切換,這似乎並不是一件難事,但隨著代碼體量的增加,它會讓你抓狂,線上上報的異常因線程使用不當導致的可不在少數。
而協程可以幫你優雅的處理掉這些。
協程本身是一個脫離語言實現的概念,我們「很嚴謹」(哈哈)的給出維基百科的定義:
簡單來說就是,協程是一種非搶佔式或者說協作式的計算機程序並發調度的實現,程序可以主動掛起或者恢復執行。這裡還是需要有點兒操作系統的知識的,我們在 Java 虛擬機上所認識到的線程大多數的實現是映射到內核的線程的,也就是說線程當中的代碼邏輯在線程搶到 CPU 的時間片的時候才可以執行,否則就得歇著,當然這對於我們開發者來說是透明的;而經常聽到所謂的協程更輕量的意思是,協程並不會映射成內核線程或者其他這麼重的資源,它的調度在用戶態就可以搞定,任務之間的調度並非搶佔式,而是協作式的。
關於並發和並行:正因為 CPU 時間片足夠小,因此即便一個單核的 CPU,也可以給我們營造多任務同時運行的假象,這就是所謂的「並發」。並行才是真正的同時運行。並發的話,更像是 Magic。
如果大家熟悉 Java 虛擬機的話,就想像一下 Thread 這個類到底是什麼吧,為什麼它的 run 方法會運行在另一個線程當中呢?誰負責執行這段代碼的呢?顯然,咋一看,Thread 其實是一個對象而已,run 方法裡面包含了要執行的代碼——僅此而已。協程也是如此,如果你只是看標準庫的 API,那麼就太抽象了,但我們開篇交代了,學習協程不要上來去接觸標準庫,kotlinx.coroutines 框架才是我們用戶應該關心的,而這個框架裡面對應於 Thread 的概念就是 Job 了,大家可以看下它的定義:
我們再來看看 Thread 的定義:
這裡我們非常貼心的省略了一些注釋和不太相關的介面。我們發現,Thread 與 Job 基本上功能一致,它們都承載了一段代碼邏輯(前者通過 run 方法,後者通過構造協程用到的 Lambda 或者函數),也都包含了這段代碼的運行狀態。
而真正調度時二者才有了本質的差異,具體怎麼調度,我們只需要知道調度結果就能很好的使用它們了。
6. 小結
我們先通過例子來引入,從大家最熟悉的代碼到協程的例子開始,演化到協程的寫法,讓大家首先能從感性上對協程有個認識,最後我們給出了協程的定義,也告訴大家協程究竟能做什麼。
這篇文章沒有追求什麼內部原理,只是企圖讓大家對協程怎麼用有個第一印象。如果大家仍然感覺到迷惑,不怕,後面我將再用幾篇文章從例子入手來帶著大家分析協程的運行,而原理的分析,會放到大家能夠熟練掌握協程之後再來探討。
![](https://pic.pimg.tw/zzuyanan/1488615166-1259157397.png)
![](https://pic.pimg.tw/zzuyanan/1482887990-2595557020.jpg)
TAG:千鋒JAVA開發學院 |