简评:可能对于不少的 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