【譯】第一次走進 Android 中的 Kotlin 協程

Feximin發表於2019-03-04

本文提取並改編自最近更新的 Kotlin for Android Developers 一書。

協程是 Kotlin 1.1 引入的最牛逼的功能。他們確實很棒,不但很強大,而且社群仍然在挖掘如何使他們得到更加充分的利用。

簡單來說,協程是一種按序寫非同步程式碼的方式。你可以一行一行地寫程式碼,而不是到處都有亂七八糟的回撥。有的還將會有暫停執行然後等待結果返回的能力。

如果你以前是 C# 程式設計師,async/await 是最接近的概念。但是 Kotlin 中的協程功能更強大,因為他們不是一個特定想法的實現,而是一個語言級別的功能,可以有多種實現去解決各種問題

你可以編寫自己的實現,或者使用一個 Kotlin 團隊和其他獨立開發者已經構建好的實現。

你要明白協程在 Kotlin 1.1 中是一個實驗性的功能。這意味著當前實現在將來可能會改變,儘管舊的實現仍將被支援,但你有可能想遷移到新的定義上。如我們稍後將見,你需要去選擇開啟這個特性,否則在使用的時候會有警告。

這也意味著你應該將本文視為一個(協程)可以做些什麼的示例而不是一個經驗法則。未來幾個月可能會有很大變動。


理解協程如何工作

本文旨在讓你瞭解一些基本概念,會用一個現有的庫,而不是去自己去實現一個。但我認為重要的是瞭解一些內部原理,這樣你就不會盲目使用了。

協程基於暫停函式的想法:那些函式被呼叫之後可以終止(程式)執行,一旦完成他們自己的任務之後又可以讓他(程式)繼續執行。

暫停函式用保留關鍵字 suspend 來標記,而且只能在其他暫停函式或協程內部被呼叫。

這意味著你不能隨便呼叫一個暫停函式。需要有一個包裹函式來構建協程並提供所需的上下文。類似這樣的:

fun <T> async(block: suspend () -> T)複製程式碼

我並不是在解釋如何實現上述方法。那是一個複雜的過程,不在本文範圍內,並且大多情況下已經有多種實現好的方法了。

如果你確實有興趣實現自己的,你可以讀一下 coroutines Github 中所寫的規範。你僅需要知道的是:方法名字可以隨意取,至少有一個暫停塊做為引數。

然後你可以實現一個暫停函式並在塊中呼叫:

suspend fun mySuspendingFun(x: Int) : Result {
 …
}

async {
 val res = mySuspendingFun(20)
 print(res)
}複製程式碼

協程是執行緒嗎?不完全是。他們的工作方式相似,但是(協程)更輕量、更有效。你可以有數以百萬的協程執行在少量的幾個執行緒中,這開啟了一個充滿可能性的世界。

使用協程功能有三種方式:

  • 原始實現:意思是建立你自己的方式去使用協程。這非常複雜並且通常不是必要的。
  • 底層實現: Kotlin 提供了一套庫,解決了一些最難的部分並提供了不同場景下的具體實現,你可以在 kotlinx.coroutines 倉庫中找到這些庫,比如說: one for Android
  • 高階實現:如果你只是想要一個可以提供一切你所需的解決方案來開始馬上使用協程的話,有幾個庫可以使用,他們為你做了所有複雜的工作,並且(庫的)數量在持續增長。我推薦 Anko,他提供了一個可以很好的工作在 Android 上的方案,有可能你已經很熟悉了。

使用 Anko 實現協程

自從 0.10 版本以來,Anko 提供了兩種方法以在 Android 上使用協程。

第一種與我們在上面的例子中看到的非常相似,和其他的庫所做的也類似。

首先,你需要建立一個可以呼叫暫停函式的非同步塊

async(UI) {
 …
}複製程式碼

UI引數是 async 塊的執行上下文。

然後你可以建立在後臺執行緒中執行的塊,將結果返回給UI執行緒。那些塊以 bg 方法定義:

async(UI) {
 val r1: Deferred<Result> = bg { fetchResult1() }
 val r2: Deferred<Result> = bg { fetchResult2() }
 updateUI(r1.await(), r2.await())
}複製程式碼

bg 返回一個 Deferred 物件,這個物件await() 方法被呼叫後會暫停協程,直到有結果返回。我們將在下面的例子中採用這種方案。

正如你可能知道的,由於 Kotlin 編譯器能夠推匯出變數型別,因此可以更加簡單:

async(UI) {
 val r1 = bg { fetchResult1() }
 val r2 = bg { fetchResult2() }
 updateUI(r1.await(), r2.await())
}複製程式碼

第二種方法是利用與特定子庫中提供的監聽器的整合,這取決於你打算使用哪個監聽器。

例如,在 anko-sdk15-coroutines 中有一個 onClick 監聽器,他的 lambda 實際上是一個協程。這樣你就可以在監聽器程式碼塊上立即使用暫停函式:

textView.onClick {
 val r1 = bg { fetchResult1() }
 val r2 = bg { fetchResult2() }
 updateUI(r1.await(), r2.await())
}複製程式碼

如你所見,結果與之前的很相似。只是少了一些程式碼。

為了使用他,你需要新增一些依賴,這取決於你想要使用哪些監聽器:

compile “org.jetbrains.anko:anko-sdk15-coroutines:$anko_version”
compile “org.jetbrains.anko:anko-appcompat-v7-coroutines:$anko_version”
compile “org.jetbrains.anko:anko-design-coroutines:$anko_version複製程式碼

在示例中使用協程

這本書所解釋的例子(你可以在這裡找到)中,我們建立了一個簡單的天氣應用。

為了使用 Anko 協程,我們首先需要新增這個新的依賴:

compile “org.jetbrains.anko:anko-coroutines:$anko_version複製程式碼

接下來,如果你還記得,我曾經告訴過你需要選擇使用這個功能,否則就會出現警告。要做到這一點(使用協程功能),只需要簡單地在根資料夾下的 gradle.properties 檔案(如果不存在就建立)中新增這一行:

kotlin.coroutines=enable複製程式碼

現在,你已經準備好開始使用協程了。讓我們首先進入詳情 activity 中。他只是使用一個特定的命令呼叫了資料庫(用來快取每週的天氣預報資料)。

這是生成的程式碼:

async(UI) {
    val id = intent.getLongExtra(ID, -1)
    val result = bg { RequestDayForecastCommand(id)
        .execute() }
    bindForecast(result.await())
}複製程式碼

太棒了!天氣預報資料是在一個後臺執行緒中請求的,這多虧了 bg 方法,這個方法返回了一個延遲結果。那個延遲結果在可以返回前會一直在 bindForecast 呼叫中等待。

但並不是一切都好。發生了什麼?協程有一個問題:他們持有一個 DetailActivity 的引用,如果這個請求永不結束就會記憶體洩露

別擔心,因為 Anko 有一個解決方案。你可以為你的 activity 建立一個弱引用,然後使用那個弱引用來代替:

val ref = asReference()
val id = intent.getLongExtra(ID, -1)

async(UI) {
 val result = bg { RequestDayForecastCommand(id).execute() }
 ref().bindForecast(result.await())
}複製程式碼

在 activity 可用時,弱引用允許訪問 activity,當 activity 被殺死,協程將會取消。需要仔細確保的是所有對 activity 中的方法或屬性的呼叫都要經過這個 ref 物件。

但是如果協程多次和 activity 互動的話會有點複雜。例如,在 MainActivity 使用這個方案將變得更加複雜。

這個 activity 將基於一個 zipCode 來呼叫一個端點來請求一週的天氣預報資料:

private fun loadForecast() {

val ref = asReference()
 val localZipCode = zipCode

async(UI) {
 val result = bg { RequestForecastCommand(localZipCode).execute() }
 val weekForecast = result.await()
 ref().updateUI(weekForecast)
 }
}複製程式碼

你不能在 bg 塊中使用 ref() ,因為在那個塊中的程式碼不是一個暫停上下文,因此你需要將 zipCode 儲存在另一個本地變數中。

老實說,我認為洩露 activity 物件 1-2 秒沒那麼糟糕,不過有可能不能成為樣板程式碼。因此如果你能確保你的後臺處理不會永遠不結束(比如,為你的伺服器請求設定一個超時)的話,不使用 asReference() 也是安全的。

這樣的話,MainActivity 將變得更加簡單:

private fun loadForecast() = async(UI) {
 val result = bg { RequestForecastCommand(zipCode).execute() }
 updateUI(result.await())
}複製程式碼

綜上,你已經可以一種非常簡單的同步方式來寫你的非同步程式碼。

這些程式碼非常簡單,但是想象一下複雜的情況:後臺操作的結果被下一個後臺操作使用,或者當你需要遍歷列表併為每一項都執行請求的時候。

所有一切都可以寫成常規的同步程式碼,寫起來、維護起來將更加容易。


關於如何充分利用協程還有很多需要學習。如果你有更多相關的經驗,請評論以讓我們更加了解協程。

如果你剛剛開始學習 Kotlin ,你可以看看我的部落格這本書,或者關注我的 TwitterLinkedIn 或者 Github


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章