簡評:可能對於不少的 Android程序員來講協程(Coroutine)並非一個熟悉的概念,更可能是和線程、回調打交道。但協程這一律念其實很早就提出來了,C#, Lua, Go 等語言也支持協程,Kotlin 也提供了 kotlinx.coroutines 庫來幫助使用協程。因此,今天這裏就介紹下怎麼經過 Kotlin 在 Android 中使用協程。html
Coroutine 中文大多翻譯爲「協程」,相關概念網上已有不少相關的資料(《計算機程序設計藝術 卷一》中就有講到 Coroutine),這裏就再也不贅述。java
在這篇文章中,主要關注如何經過 kotlinx.coroutines 庫來在 Android 中實現 Coroutine。android
如何啓動一個協程(Coroutine)git
在 kotlinx.coroutines 庫中,咱們能夠使用 launch 或 async 來啓動一個新的 coroutine。程序員
從概念上講,async 和 launch 是相似的,區別在於 launch 會返回一個 Job 對象,不會攜帶任何結果值。而 async 則是返回一個 Deferred - 一個輕量級、非阻塞的 future,表明了以後將會提供結果值的承諾(promise),所以能夠使用 .await() 來得到其最終的結果,固然 Deferred 也是一個 Job,若是須要也是能夠取消的。github
若是你對於 future, promise, deferred 等概念感到困惑,能夠先閱讀併發 Promise 模型或其餘資料瞭解相關概念。segmentfault
Coroutine contextapi
在 Android 中咱們常常使用兩類 context:promise
// dispatches execution onto the Android main UI thread private val uiContext: CoroutineContext = UI // represents a common pool of shared threads as the coroutine dispatcher private val bgContext: CoroutineContext = CommonPool
這裏 bgContext 使用 CommonPool,能夠限制同時運行的線程數小於 Runtime.getRuntime.availableProcessors() - 1。併發
launch + async (execute task)
父協程(The parent coroutine)使用 uiContext 經過 launch 啓動。
子協程(The child coroutine)使用 CommonPool context 經過 async 啓動。
注意:
- 父協程老是會等待全部的子協程執行完畢。
- 若是發生未檢查的異常,應用將會崩潰。
下面實現一個簡單的讀取數據並由視圖進行展現的例子:
private fun loadData() = launch(uiContext) { view.showLoading() // ui thread val task = async(bgContext) { dataProvider.loadData("Task") } val result = task.await() // non ui thread, suspend until finished view.showData(result) // ui thread }
launch + async + async (順序執行兩個任務)
下面的兩個任務是順序執行的:
private fun loadData() = launch(uiContext) { view.showLoading() // ui thread // non ui thread, suspend until task is finished val result1 = async(bgContext) { dataProvider.loadData("Task 1") }.await() // non ui thread, suspend until task is finished val result2 = async(bgContext) { dataProvider.loadData("Task 2") }.await() val result = "$result1 $result2" // ui thread view.showData(result) // ui thread }
launch + async + async (同時執行兩個任務)
private fun loadData() = launch(uiContext) { view.showLoading() // ui thread val task1 = async(bgContext) { dataProvider.loadData("Task 1") } val task2 = async(bgContext) { dataProvider.loadData("Task 2") } val result = "${task1.await()} ${task2.await()}" // non ui thread, suspend until finished view.showData(result) // ui thread }
啓動 coroutine 並設置超時時間
能夠經過 withTimeoutOrNull 來給 coroutine job 設置時限,若是超時將會返回 null。
private fun loadData() = launch(uiContext) { view.showLoading() // ui thread val task = async(bgContext) { dataProvider.loadData("Task") } // non ui thread, suspend until the task is finished or return null in 2 sec val result = withTimeoutOrNull(2, TimeUnit.SECONDS) { task.await() } view.showData(result) // ui thread }
如何取消一個協程(coroutine)
var job: Job? = null fun startPresenting() { job = loadData() } fun stopPresenting() { job?.cancel() } private fun loadData() = launch(uiContext) { view.showLoading() // ui thread val task = async(bgContext) { dataProvider.loadData("Task") } val result = task.await() // non ui thread, suspend until finished view.showData(result) // ui thread }
當父協程被取消時,全部的子協程將遞歸的被取消。
在上面的例子中,若是 stopPresenting 在被調用時 dataProvider.loadData 正在運行,那麼 view.showData 方法將不會被調用。
如何處理異常
try-catch block
咱們仍是能夠像平時同樣用 try-catch 來捕獲和處理異常。不過這裏推薦將 try-catch 移到 dataProvider.loadData 方法裏面,而不是直接包裹在外面,並提供一個統一的 Result 類方便處理。
data class Result<out T>(val success: T? = null, val error: Throwable? = null) private fun loadData() = launch(uiContext) { view.showLoading() // ui thread val task = async(bgContext) { dataProvider.loadData("Task") } val result: Result<String> = task.await() // non ui thread, suspend until the task is finished if (result.success != null) { view.showData(result.success) // ui thread } else if (result.error != null) { result.error.printStackTrace() } }
async + async
當經過 async 來啓動父協程時,將會忽略掉任何異常:
private fun loadData() = async(uiContext) { view.showLoading() // ui thread val task = async(bgContext) { dataProvider.loadData("Task") } val result = task.await() // non ui thread, suspend until the task is finished view.showData(result) // ui thread }
在這裏 loadData() 方法會返回 Job 對象,而 exception 會被存放在這個 Job 對象中,咱們能夠用 invokeOnCompletion 函數來進行檢索:
var job: Job? = null fun startPresenting() { job = loadData() job?.invokeOnCompletion { it: Throwable? -> it?.printStackTrace() // (1) // or job?.getCompletionException()?.printStackTrace() // (2) // difference between (1) and (2) is that (1) will NOT contain CancellationException // in case if job was cancelled } }
launch + coroutine exception handler
咱們還能夠爲父協程的 context 中添加 CoroutineExceptionHandler 來捕獲和處理異常:
val exceptionHandler: CoroutineContext = CoroutineExceptionHandler { _, throwable -> throwable.printStackTrace() } private fun loadData() = launch(uiContext + exceptionHandler) { view.showLoading() // ui thread val task = async(bgContext) { dataProvider.loadData("Task") } val result = task.await() // non ui thread, suspend until the task is finished view.showData(result) // ui thread }
怎麼測試協程(coroutine)?
要啓動協程(coroutine)必需要指定一個 CoroutineContext。
class MainPresenter(private val view: MainView, private val dataProvider: DataProviderAPI) { private fun loadData() = launch(UI) { // UI - dispatches execution onto Android main UI thread view.showLoading() // CommonPool - represents common pool of shared threads as coroutine dispatcher val task = async(CommonPool) { dataProvider.loadData("Task") } val result = task.await() view.showData(result) } }
所以,若是你想要爲你的 MainPresenter 寫單元測試,你就必需要能爲 UI 和 後臺任務指定 coroutine context。
最簡單的方式就是爲 MainPresenter 的構造方法增長兩個參數,並設置默認值:
class MainPresenter(private val view: MainView, private val dataProvider: DataProviderAPI private val uiContext: CoroutineContext = UI, private val ioContext: CoroutineContext = CommonPool) { private fun loadData() = launch(uiContext) { // use the provided uiContext (UI) view.showLoading() // use the provided ioContext (CommonPool) val task = async(bgContext) { dataProvider.loadData("Task") } val result = task.await() view.showData(result) } }
如今,就能夠在測試中傳入 kotlin.coroutines 提供的 EmptyCoroutineContext 來讓代碼運行在當前線程裏。
@Test fun test() { val dataProvider = Mockito.mock(DataProviderAPI::class.java) val mockView = Mockito.mock(MainView::class.java) val presenter = MainPresenter(mockView, dataProvider, EmptyCoroutineContext, EmptyCoroutineContext) presenter.startPresenting() ... }
上面就是 kotlin 中協程(coroutine)的基本用法,完整代碼能夠查看 Github 項目。
若是想了解更多內容還能夠查看 Kotlin 的官方示例:Kotlin/kotlinx.coroutines