专栏首页Android补给站What? 你还不知道Kotlin Coroutine?

What? 你还不知道Kotlin Coroutine?

Rouse

读完需要

16

分钟

速读仅需6分钟

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

1

What Coroutine

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

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

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

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

2

Callback

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

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

1        GatheringVoiceSettingRepository.getInstance().getGeneralSettings(RequestLanguage::class.java)
2                .observe(this, { language ->
3                    convertResult(language, { enable -> 
4                        // todo something
5                    })
6                })

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

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

3

Rx系列

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

1        disposable = createCall().map {
2            // return RequestType
3        }.subscribeWith(object : SMDefaultDisposableObserver<RequestType>{
4            override fun onNext(t: RequestType) {
5                // todo something
6            }
7        })

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

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

4

Coroutine在Kotlin中的基本要点

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

1    private suspend fun get(url: String) = withContext(Dispatchers.IO) {
2        // to do network request
3        url
4    }
5
6    private suspend fun fetch() { // 在Main中调用
7        val result = get("https://rousetime.com") // 在IO中调用
8        showToast(result) // 在Main中调用
9    }

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

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

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

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

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

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

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

4.1

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的解析。

再来看上面的示例

1    private suspend fun get(url: String) = withContext(Dispatchers.IO) {
2        // to do network request
3        url
4    }
5
6    private suspend fun fetch() { // 在Main中调用
7        val result = get("https://rousetime.com") // 在IO中调用
8        showToast(result) // 在Main中调用
9    }

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

4.2

创建Coroutine

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

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

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

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

1    private fun excute() {
2        CoroutineScope(Dispatchers.Main).launch {
3            fetch()
4        }
5    }

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

 1    private suspend fun fetchAll() {
 2        coroutineScope {
 3            val deferredFirst = async { get("first") }
 4            val deferredSecond = async { get("second") }
 5            deferredFirst.await()
 6            deferredSecond.await()
 7
 8//            val deferred = listOf(
 9//                    async { get("first") },
10//                    async { get("second") }
11//            )
12//            deferred.awaitAll()
13        }
14    }

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

5

Architecture Components

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

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

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

 1object Versions {
 2    const val arch_version = "2.2.0-alpha01"
 3    const val arch_room_version = "2.1.0-rc01"
 4}
 5
 6object Dependencies {
 7    val arch_lifecycle = "androidx.lifecycle:lifecycle-extensions:${Versions.arch_version}"
 8    val arch_viewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.arch_version}"
 9    val arch_livedata = "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.arch_version}"
10    val arch_runtime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.arch_version}"
11    val arch_room_runtime = "androidx.room:room-runtime:${Versions.arch_room_version}"
12    val arch_room_compiler = "androidx.room:room-compiler:${Versions.arch_room_version}"
13    val arch_room = "androidx.room:room-ktx:${Versions.arch_room_version}"
14}

5.1

ViewModelScope

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

1    fun getAll() {
2        viewModelScope.launch {
3            val articleList = withContext(Dispatchers.IO) {
4                articleDao.getAll()
5            }
6            adapter.clear()
7            adapter.addAllData(articleList)
8        }
9    }

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

5.2

LifecycleScope

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

1    private fun coroutine() {
2        lifecycleScope.launch {
3            delay(2000)
4            showToast("coroutine first")
5            delay(2000)
6            showToast("coroutine second")
7        }
8    }

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

lifecycleScope本质是Lifecycle.coroutineScope

1val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
2    get() = lifecycle.coroutineScope
3
4    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
5        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
6            lifecycle.removeObserver(this)
7            coroutineContext.cancel()
8        }
9    }

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

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

 1    private fun coroutine() {
 2        lifecycleScope.launchWhenStarted {
 3
 4        }
 5        lifecycleScope.launch {
 6            whenStarted {  }
 7            delay(2000)
 8            showToast("coroutine first")
 9            delay(2000)
10            showToast("coroutine second")
11        }
12    }

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

5.3

LiveData

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

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

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

 1    // Room
 2    @Query("SELECT * FROM article_model WHERE title = :title LIMIT 1")
 3    fun findByTitle(title: String): ArticleModel?
 4    // ViewModel
 5    fun findByTitle(title: String) = liveData(Dispatchers.IO) {
 6        MyApp.db.articleDao().findByTitle(title)?.let {
 7            emit(it)
 8        }
 9    }
10    // Activity
11    private fun checkArticle() {
12        vm.findByTitle("Android Architecture Components Part1:Room").observe(this, Observer {
13        })
14    }

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

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

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

最后我使用Architecture Component与Coroutine写了个简单的Demo,大家可以在Github中或者点击阅读原文查看。

源码地址: https://github.com/idisfkj/android-api-analysis

本文分享自微信公众号 - Android补给站(PayneDev),作者:Rouse

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • What? 你还不知道Kotlin Coroutine?

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

    Rouse
  • 只需三步实现Databinding插件化

    首先为何我要实现Databinding这个小插件,主要是在日常开发中,发现每次通过Android Studio的Layout resource file来创建x...

    Rouse
  • ViewDragHelper之手势操作神器

    在Android中避免不了自定义ViewGroup,来实现我们原生控件所不能满足的需求。尤其是复杂的ViewGroup实现,手势的处理是避免不了的。我们要针对不...

    Rouse
  • What? 你还不知道Kotlin Coroutine?

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

    Rouse
  • 吉利花90亿美元成奔驰母公司最大股东,背后打的什么算盘?

    作者 | 阿司匹林 李书福又出手了,这一次是梅赛德斯-奔驰的母公司——戴姆勒。 据《金融时报》报道,戴姆勒在一份监管申报文件中披露,吉利已经持有戴姆勒 9....

    AI科技大本营
  • 点云存储文件格式简介

    在众多存储点云的文件格式中,有些格式是为点云数据“量身打造”的,也有一些文件格式(如计算机图形学和计算机和学领域的3D模型或通讯数据文件)具备表示...

    点云PCL博主
  • Django中的信号

    Django中内置的signal Django中提供了"信号调度",用于在框架执行操作时解耦. 一些动作发生的时候,系统会根据信号定义的函数执行相应的操作 Mo...

    用户1214487
  • Kotlin学习笔记(二)-程序结构(上 )

    上节我们主要讲了Kotlin的数据类型,这节我们主要从程序结构,包括方法,类成员,运算符的角度去认识Kotlin

    g小志
  • LeetCode 881. 救生艇(贪心,双指针)

    第 i 个人的体重为 people[i],每艘船可以承载的最大重量为 limit。

    Michael阿明
  • IntelliJ IDEA无法从Controller跳转到视图页面的解决方案

    一般情况下,配置完上面就可以正常导航了,但是今天要说的不是一般情况,否则也就不说了,如果经过第一步设置后,还是不能正常导航的同学,可以接着看第二步。

    Java架构师历程

扫码关注云+社区

领取腾讯云代金券