What-你还不知道Kotlin-Coroutine

14次阅读

共计 6490 个字符,预计需要花费 17 分钟才能阅读完成。

今天我们来聊聊 Kotlin Coroutine,如果你还没有了解过,那么我要提前恭喜你,因为你将掌握一个新技能,对你的代码方面的提升将是很好的助力。

What Coroutine

简单的来说,Coroutine 是一个并发的设计模式,你能通过它使用更简洁的代码来解决异步问题。

例如,在 Android 方面它主要能够帮助你解决以下两个问题:

  1. 在主线程中执行耗时任务导致的主线程阻塞,从而使 App 发生 ANR。
  2. 提供主线程安全,同时对来自于主线程的网络回调、磁盘操提供保障。

这些问题,在接下来的文章中我都会给出解决的示例。

Callback

说到异步问题,我们先来看下我们常规的异步处理方式。首先第一种是最基本的 callback 方式。

callback 的好处是使用起来简单,但你在使用的过程中可能会遇到如下情形

        GatheringVoiceSettingRepository.getInstance().getGeneralSettings(RequestLanguage::class.java)
                .observe(this, { language ->
                    convertResult(language, { enable -> 
                        // todo something
                    })
                })

这种在其中一个 callback 中回调另一个 callback 回调,甚至更多的 callback 都是可能存在。这些情况导致的问题是代码间的嵌套层级太深,导致逻辑嵌套复杂,后续的维护成本也要提高,这不是我们所要看到的。

那么有什么方法能够解决呢?当然有,其中的一种解决方法就是我接下来要说的第二种方式。

Rx 系列

对多嵌套回调,Rx 系列在这方面处理的已经非常好了,例如 RxJava。下面我们来看一下 RxJava 的解决案例

        disposable = createCall().map {// return RequestType}.subscribeWith(object : SMDefaultDisposableObserver<RequestType>{override fun onNext(t: RequestType) {// todo something}
        })

RxJava 丰富的操作符,再结合 Observable 与 Subscribe 能够很好的解决异步嵌套回调问题。但是它的使用成本就相对提高了,你要对它的操作符要非常了解,避免在使用过程中滥用或者过度使用,这样自然复杂度就提升了。

那么我们渴望的解决方案是能够更加简单、全面与健壮,而我们今天的主题 Coroutine 就能够达到这种效果。

Coroutine 在 Kotlin 中的基本要点

在 Android 里,我们都知道网络请求应该放到子线程中,相应的回调处理一般都是在主线程,即 ui 线程。正常的写法就不多说了,那么使用 Coroutine 又该是怎么样的呢?请看下面代码示例:

    private suspend fun get(url: String) = withContext(Dispatchers.IO) {
        // to do network request
        url
    }
 
    private suspend fun fetch() { // 在 Main 中调用
        val result = get("https://rousetime.com") // 在 IO 中调用
        showToast(result) // 在 Main 中调用
    }

如果 fetch 方法在主线程调用,那么你会发现使用 Coroutine 来处理异步回调就像是在处理同步回调一样,简洁明了、行云流水,同时再也没有嵌套的逻辑了。

注意看方法,Coroutine 为了能够实现这种简单的操作,增加了两个操作来解决耗时任务,分别为 suspend 与 resume

  • suspend: 挂起当前执行的协同程序,并且保存此刻的所有本地变量
  • resume: 从它被挂起的位置继续执行,并且挂起时保存的数据也被还原

解释的有点生硬,简单的来说就是 suspend 可以将该任务挂起,使它暂时不在调用的线程中,以至于当前线程可以继续执行别的任务,一旦被挂起的任务已经执行完毕,那么就会通过 resume 将其重新插入到当前线程中。

所以上面的示例展示的是,当 get 还在请求的时候,fetch 方法将会被挂起,直到 get 结束,此时才会插入到主线程中并返回结果。

一图胜千言,我做了一张图,希望能有所帮助。

另外需要注意的是,suspend 方法只能够被其它的 suspend 方法调用或者被一个 coroutine 调用,例如 launch。

Dispatchers

另一方面 Coroutine 使用 Dispatchers 来负责调度协调程序执行的线程,这一点与 RxJava 的 schedules 有点类似,但不同的是 Coroutine 一定要执行在 Dispatchers 调度中,因为 Dispatchers 将负责 resume 被 suspend 的任务。

Dispatchers 提供三种模式切换,分别为

  1. Dispatchers.Main: 使 Coroutine 运行中主线程,以便 UI 操作
  2. Dispatchers.IO: 使 Coroutine 运行在 IO 线程,以便执行网络或者 I / O 操作
  3. Dispatchers.Default: 在主线程之外提高对 CPU 的利用率,例如对 list 的排序或者 JSON 的解析。

再来看上面的示例

    private suspend fun get(url: String) = withContext(Dispatchers.IO) {
        // to do network request
        url
    }
 
    private suspend fun fetch() { // 在 Main 中调用
        val result = get("https://rousetime.com") // 在 IO 中调用
        showToast(result) // 在 Main 中调用
    }

为了让 get 操作运行在 IO 线程,我们使用 withContext 方法,对该方法传入 Dispatchers.IO,使得它闭包下的任务都处于 IO 线程中,同时 witchContext 也是一个 suspend 函数。

创建 Coroutine

上面提到 suspend 函数只能在相应的 suspend 中或者 Coroutine 中调用。那么 Coroutine 又该如何创建呢?

有两种方式,分别为 launch 与 async

  1. launch: 开启一个新的 Coroutine,但不返回结果
  2. async: 开启一个新的 Coroutine,但返回结果

还是上面的例子,如果我们需要执行 fetch 方法,可以使用 launch 创建一个 Coroutine

    private fun excute() {CoroutineScope(Dispatchers.Main).launch {fetch()
        }
    }

另一种 async,因为它返回结果,如果要等所有 async 执行完毕,可以使用 await 或者 awaitAll

    private suspend fun fetchAll() {
        coroutineScope {val deferredFirst = async { get("first") }
            val deferredSecond = async {get("second") }
            deferredFirst.await()
            deferredSecond.await()

//            val deferred = listOf(//                    async { get("first") },
//                    async {get("second") }
//            )
//            deferred.awaitAll()}
    }

所以通过 await 或者 awaitAll 可以保证所有 async 完成之后再进行 resume 调用。

Architecture Components

如果你使用了 Architecture Component,那么你也可以在其基础上使用 Coroutine,因为 Kotlin Coroutine 已经提供了相应的 api 并且定制了 CoroutineScope。

如果你还不了解 Architecture Component,强烈推荐你阅读我的 Android Architecture Components 系列

在使用之前,需要更新 architecture component 的依赖版本,如下所示

object Versions {
    const val arch_version = "2.2.0-alpha01"
    const val arch_room_version = "2.1.0-rc01"
}
 
object Dependencies {val arch_lifecycle = "androidx.lifecycle:lifecycle-extensions:${Versions.arch_version}"
    val arch_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.arch_version}"
    val arch_livedata = "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.arch_version}"
    val arch_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.arch_version}"
    val arch_room_runtime = "androidx.room:room-runtime:${Versions.arch_room_version}"
    val arch_room_compiler = "androidx.room:room-compiler:${Versions.arch_room_version}"
    val arch_room = "androidx.room:room-ktx:${Versions.arch_room_version}"
}

ViewModelScope

在 ViewModel 中,为了能够使用 Coroutine 提供了 viewModelScope.launch,同时一旦 ViewModel 被清除,对应的 Coroutine 也会自动取消。

    fun getAll() {
        viewModelScope.launch {val articleList = withContext(Dispatchers.IO) {articleDao.getAll()
            }
            adapter.clear()
            adapter.addAllData(articleList)
        }
    }

在 IO 线程通过 articleDao 从数据库取数据,一旦数据返回,在主线程进行处理。如果在取数据的过程中 ViewModel 已经清除了,那么数据获取也会停止,防止资源的浪费。

LifecycleScope

对于 Lifecycle,提供了 LifecycleScope,我们可以直接通过 launch 来创建 Coroutine

    private fun coroutine() {
        lifecycleScope.launch {delay(2000)
            showToast("coroutine first")
            delay(2000)
            showToast("coroutine second")
        }
    }

因为 Lifecycle 是可以感知组件的生命周期的,所以一旦组件 onDestroy 了,相应的 LifecycleScope.launch 闭包中的调用也将取消停止。

lifecycleScope 本质是 Lifecycle.coroutineScope

val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
 
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {lifecycle.removeObserver(this)
            coroutineContext.cancel()}
    }

它会在 onStateChanged 中监听 DESTROYED 状态,同时调用 cancel 取消 Coroutine。

另一方面,lifecycleScope 还可以根据 Lifecycle 不同的生命状态进行 suspend 处理。例如对它的 STARTED 进行特殊处理

    private fun coroutine() {lifecycleScope.launchWhenStarted {}
        lifecycleScope.launch {whenStarted {}
            delay(2000)
            showToast("coroutine first")
            delay(2000)
            showToast("coroutine second")
        }
    }

不管是直接调用 launchWhenStarted 还是在 launch 中调用 whenStarted 都能达到同样的效果。

LiveData

LiveData 中可以直接使用 liveData,在它的参数中会调用一个 suspend 函数,同时会返回 LiveData 对象

fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope<T>.() -> Unit): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

所以我们可以直接使用 liveData 来是实现 Coroutine 效果,我们来看下面一段代码

    // Room
    @Query("SELECT * FROM article_model WHERE title = :title LIMIT 1")
    fun findByTitle(title: String): ArticleModel?
    // ViewModel
    fun findByTitle(title: String) = liveData(Dispatchers.IO) {MyApp.db.articleDao().findByTitle(title)?.let {emit(it)
        }
    }
    // Activity
    private fun checkArticle() {vm.findByTitle("Android Architecture Components Part1:Room").observe(this, Observer {})
    }

通过 title 从数据库中取数据,数据的获取发生在 IO 线程,一旦数据返回,再通过 emit 方法将返回的数据发送出去。所以在 View 层,我们可以直接使用 checkArticle 中的方法来监听数据的状态。

另一方面 LiveData 有它的 active 与 inactive 状态,对于 Coroutine 也会进行相应的激活与取消。对于激活,如果它已经完成了或者非正常的取消,例如抛出 CancelationException 异常,此时将不会自动激活。

对于发送数据,还可以使用 emitSource,它与 emit 共同点是在发送新的数据之前都会将原数据清除,而不同点是,emitSource 会返回一个 DisposableHandle 对象,以便可以调用它的 dispose 方法进行取消发送。

最后我使用 Architecture Component 与 Coroutine 写了个简单的 Demo,大家可以在 Github 中进行查看

源码地址: https://github.com/idisfkj/an…

推荐阅读

Android Architecture Components Part1:Room

Android Architecture Components Part2:LiveData

Android Architecture Components Part3:Lifecycle

Android Architecture Components Part4:ViewModel

公众号

扫描二维码,关注微信公众号,获取独家最新 IT 技术!

正文完
 0