资讯专栏INFORMATION COLUMN

What? 你还不知道Kotlin Coroutine?

yunhao / 1399人阅读

摘要:例如,在方面它主要能够帮助你解决以下两个问题在主线程中执行耗时任务导致的主线程阻塞,从而使发生。提供主线程安全,同时对来自于主线程的网络回调磁盘操提供保障。在线程通过从数据库取数据,一旦数据返回,在主线程进行处理。

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

What Coroutine

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

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

在主线程中执行耗时任务导致的主线程阻塞,从而使App发生ANR。

提供主线程安全,同时对来自于主线程的网络回调、磁盘操提供保障。

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

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{
            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提供三种模式切换,分别为

Dispatchers.Main: 使Coroutine运行中主线程,以便UI操作

Dispatchers.IO: 使Coroutine运行在IO线程,以便执行网络或者I/O操作

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

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

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  liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope.() -> Unit
): LiveData = 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技术!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/75042.html

相关文章

  • 在 Android 中使用协程(Coroutine

    摘要:在中我们经常使用两类用于执行相关操作。这里使用,可以限制同时运行的线程数小于。子协程使用通过启动。注意父协程总是会等待所有的子协程执行完毕。如果发生未检查的异常,应用将会崩溃。上面就是中协程的基本用法,完整代码可以查看项目。 showImg(https://segmentfault.com/img/bVW8vs?w=1240&h=698); 简评:可能对于很多的 Android程序员来...

    K_B_Z 评论0 收藏0
  • Kotlin 资源大全

    摘要:联系方式开发交流群扫描二维码添加小编好友,备注,稍后会拉你进群 目录 介绍 官网及文档 中文社区 教程 & 文章 开源库和框架 Demo 其他 介绍 今天凌晨的 Google I/O 上,Google 正式宣布官方支持 Kotlin. 为了让大家更快了解和上手 Kotlin,掘金技术社区为大家整理了这份 Kotlin 资源大全,希望可以帮助大家用最短时间学习 Kotlin. 官...

    VPointer 评论0 收藏0
  • 作为前端开发者,还不知道什么是 postCss?

    摘要:的定位属于预处理器吗还是属于后置处理器都不是,因为具体做的事取决于开发者使用了什么插件。这里做一个我觉得比较恰当的类比,中的相当于的中的,,等预处理器相当于,虽然不是完全合理,但是还是比较恰当。 前言 原谅我取这样的标题,我知道 postCss 对于大多数前端开发者来说早已经很熟悉了,但是楼主作为一个初出茅庐的前端er,还有好多的工具和技术没接触过,说来惭愧。虽然平时也喜欢使用css预...

    appetizerio 评论0 收藏0
  • 【Bugly 技术干货】Android开发必备知识:为什么说Kotlin值得一试

    摘要:的空安全设计,主要是在类型后面加表示可空,否则就不能为。换句话说,这里的提供了初始化的方法,不过真正初始化这个动作发生的时机却是在第一次被使用时了。至于技术,实际上是的一个应用,也就是属性代理了。 1、Hello, Kotlin Bugly 技术干货系列内容主要涉及移动开发方向,是由 Bugly 邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明...

    hizengzeng 评论0 收藏0
  • webpack 大法好 ---- what`s webpack?(前言)

    摘要:原始的开发模式已经满足不了呈指数增长的需求了。它承担起了模块管理这一重要角色。是个前端小菜鸟,接触前端不到两年时间,去年毕业正式参加工作。目前就职于杭州边锋网络神盾局就是这么霸气。 对于刚进入前端领域的人,特别是还处于小白阶段的初学者来说,很多人对 webpack 并不熟知。就像 Light (对,我就是 Light)一样,刚接触前端,最关心的就是样式和简单的交互了。那时候怎么会知道像...

    wwolf 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<