在 Android 中使用协程(Coroutine)

在 Android 中使用协程(Coroutine),第1张

概述 image简评:可能对于很多的Android程序员来说协程(Coroutine)并不是一个熟悉的概念,更多是和线程、回调打交道。但协程这一概念其实很早就提出来了,C#,Lua,Go等语言也支持协程,Kotlin也提供了kotlinx.coroutines库来帮助使用协程。所以,今天这里就介绍下怎么通过Kotli  image

简评:可能对于很多的 AndroID 程序员来说协程(Coroutine)并不是一个熟悉的概念,更多是和线程、回调打交道。但协程这一概念其实很早就提出来了,C#, Lua, Go 等语言也支持协程,Kotlin 也提供了 kotlinx.coroutines 库来帮助使用协程。所以,今天这里就介绍下怎么通过 Kotlin 在 AndroID 中使用协程。

Coroutine 中文大多翻译为「协程」,相关概念网上已有很多相关的资料(《计算机程序设计艺术 卷一》中就有讲到 Coroutine),这里就不再赘述。

在这篇文章中,主要关注如何通过 kotlinx.coroutines 库来在 AndroID 中实现 Coroutine。

如何启动一个协程(Coroutine)

kotlinx.coroutines 库中,我们可以使用 launch 或 async 来启动一个新的 coroutine。

从概念上讲,async 和 launch 是类似的,区别在于 launch 会返回一个 Job 对象,不会携带任何结果值。而 async 则是返回一个 Deferred - 一个轻量级、非阻塞的 future,代表了之后将会提供结果值的承诺(promise),因此可以使用 .await() 来获得其最终的结果,当然 Deferred 也是一个 Job,如果需要也是可以取消的。

如果你对于 future, promise, deferred 等概念感到困惑,可以先阅读并发 Promise 模型或其他资料了解相关概念。

Coroutine context

在 AndroID 中我们经常使用两类 context:

uiContext: 用于执行 UI 相关 *** 作。bgContext: 用于执行需要在后台运行的耗时 *** 作。
// dispatches execution onto the AndroID main UI threadprivate val uiContext: CoroutineContext = UI// represents a common pool of shared threads as the coroutine dispatcherprivate 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? = nullfun 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? = nullfun 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 来让代码运行在当前线程里。

@Testfun 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 项目。



总结

以上是内存溢出为你收集整理的在 Android 中使用协程(Coroutine)全部内容,希望文章能够帮你解决在 Android 中使用协程(Coroutine)所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1060185.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-25
下一篇 2022-05-25

发表评论

登录后才能评论

评论列表(0条)

保存