Kotlin:withContext()协同与Async-await

2020-11-11 14:58:42

看完kotin的教程,协同操作,是我学到的最新颖的。


参考地址 Kotlin协程

什么是协程?

官方描述:协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

协程很重要的一点就是当它挂起的时候,它不会阻塞其他线程。协程底层库也是异步处理阻塞任务,但是这些复杂的操作被底层库封装起来,协程代码的程序流是顺序的,不再需要一堆的回调函数,就像同步代码一样,也便于理解、调试和开发。它是可控的,线程的执行和结束是由操作系统调度的,而协程可以手动控制它的执行和结束。

使用

首先需要添加依赖:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"

1.runBlocking:T

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.e(TAG, "主线程id:${mainLooper.thread.id}")
    test()
    Log.e(TAG, "协程执行结束")}private fun test() = runBlocking {
    repeat(8) {
        Log.e(TAG, "协程执行$it 线程id:${Thread.currentThread().id}")
        delay(1000)
    }}

runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。当协程执行结束之后,页面才会被显示出来。

2.launch:Job

这是最常用的用于启动协程的方式,它最终返回一个Job类型的对象,这个Job类型的对象实际上是一个接口,它包涵了许多我们常用的方法。下面先看一下简单的使用:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.e(TAG, "主线程id:${mainLooper.thread.id}")
    val job = GlobalScope.launch {
        delay(6000)
        Log.e(TAG, "协程执行结束 -- 线程id:${Thread.currentThread().id}")
    }
    Log.e(TAG, "主线程执行结束")}//Job中的方法job.isActive
job.isCancelled
job.isCompleted
job.cancel()jon.join()

从执行结果看出,launch不会阻断主线程。

我们看一下launch方法的定义:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine}

从方法定义中可以看出,launch()CoroutineScope的一个扩展函数,CoroutineScope简单来说就是协程的作用范围。launch方法有三个参数:1.协程下上文;2.协程启动模式;3.协程体:block是一个带接收者的函数字面量,接收者是CoroutineScope

1.协程下上文

上下文可以有很多作用,包括携带参数,拦截协程执行等等,多数情况下我们不需要自己去实现上下文,只需要使用现成的就好。上下文有一个重要的作用就是线程切换Kotlin协程使用调度器来确定哪些线程用于协程执行,Kotlin提供了调度器给我们使用:

  • Dispatchers.Main:使用这个调度器在 Android 主线程上运行一个协程。可以用来更新UI 。在UI线程中执行

  • Dispatchers.IO:这个调度器被优化在主线程之外执行磁盘或网络 I/O。在线程池中执行

  • Dispatchers.Default:这个调度器经过优化,可以在主线程之外执行 cpu 密集型的工作。例如对列表进行排序和解析 JSON。在线程池中执行

  • Dispatchers.Unconfined:在调用的线程直接执行。

调度器实现了CoroutineContext接口

2.启动模式

Kotlin协程当中,启动模式定义在一个枚举类中:

public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    @ExperimentalCoroutinesApi
    ATOMIC,
    @ExperimentalCoroutinesApi
    UNDISPATCHED;}

一共定义了4种启动模式,下表是含义介绍:

启动模式作用
DEFAULT默认的模式,立即执行协程体
LAZY只有在需要的情况下运行
ATOMIC立即执行协程体,但在开始运行之前无法取消
UNDISPATCHED立即在当前线程执行协程体,直到第一个 suspend 调用

2.协程体

协程体是一个用suspend关键字修饰的一个无参,无返回值的函数类型。被suspend修饰的函数称为挂起函数,与之对应的是关键字resume(恢复),注意:挂起函数只能在协程中和其他挂起函数中调用,不能在其他地方使用。

suspend函数会将整个协程挂起,而不仅仅是这个suspend函数,也就是说一个协程中有多个挂起函数时,它们是顺序执行的。看下面的代码示例:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    GlobalScope.launch {
        val token = getToken()
        val userInfo = getUserInfo(token)
        setUserInfo(userInfo)
    }
    repeat(8){
        Log.e(TAG,"主线程执行$it")
    }}private fun setUserInfo(userInfo: String) {
    Log.e(TAG, userInfo)}private suspend fun getToken(): String {
    delay(2000)
    return "token"}private suspend fun getUserInfo(token: String): String {
    delay(2000)
    return "$token - userInfo"}

getToken方法将协程挂起,协程中其后面的代码永远不会执行,只有等到getToken挂起结束恢复后才会执行。同时协程挂起后不会阻塞其他线程的执行。

3.async

asynclaunch的用法基本一样,区别在于:async的返回值是Deferred,将最后一个封装成了该对象。async可以支持并发,此时一般都跟await一起使用,看下面的例子。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    GlobalScope.launch {
        val result1 = GlobalScope.async {
            getResult1()
        }
        val result2 = GlobalScope.async {
            getResult2()
        }
        val result = result1.await() + result2.await()
        Log.e(TAG,"result = $result")
    }}private suspend fun getResult1(): Int {
    delay(3000)
    return 1}private suspend fun getResult2(): Int {
    delay(4000)
    return 2}

async是不阻塞线程的,也就是说getResult1getResult2是同时进行的,所以获取到result的时间是4s,而不是7s。

应用

项目中的网络请求框架大部分都是基于RxJava + Retrofit + Okhttp封装的,RxJava可是很好的实现线程之间的切换,如果只是网络框架中用到了RxJava,那就是“大材小用”了,毕竟RxJava的功能还是很强大的。Retrofit2.6.0开始已经支持协程了:可以定义成一个挂起函数。

interface Api {
    @POST("user/login")
    suspend fun login(): Call<User>}

下面的例子是使用协程来代替RxJava实现线程切换。

1.首先定义一个请求相关的支持DSL语法的接收者。

class RetrofitCoroutineDSL<T> {

    var api: (Call<Result<T>>)? = null
    internal var onSuccess: ((T) -> Unit)? = null
        private set
    internal var onFail: ((msg: String, errorCode: Int) -> Unit)? = null
        private set
    internal var onComplete: (() -> Unit)? = null
        private set

    /**
     * 获取数据成功
     * @param block (T) -> Unit
     */
    fun onSuccess(block: (T) -> Unit) {
        this.onSuccess = block    }

    /**
     * 获取数据失败
     * @param block (msg: String, errorCode: Int) -> Unit
     */
    fun onFail(block: (msg: String, errorCode: Int) -> Unit) {
        this.onFail = block    }

    /**
     * 访问完成
     * @param block () -> Unit
     */
    fun onComplete(block: () -> Unit) {
        this.onComplete = block    }

    internal fun clean() {
        onSuccess = null
        onComplete = null
        onFail = null
    }}
2.然后给协程定义一个扩展方法,用于Retrofit网络请求。

fun <T> CoroutineScope.retrofit(dsl: RetrofitCoroutineDSL<T>.() -> Unit) {
    //在主线程中开启协程
    this.launch(Dispatchers.Main) {
        val coroutine = RetrofitCoroutineDSL<T>().apply(dsl)
        coroutine.api?.let { call ->
            //async 并发执行 在IO线程中
            val deferred = async(Dispatchers.IO) {
                try {
                    call.execute() //已经在io线程中了,所以调用Retrofit的同步方法
                } catch (e: ConnectException) {
                    coroutine.onFail?.invoke("网络连接出错", -1)
                    null
                } catch (e: IOException) {
                    coroutine.onFail?.invoke("未知网络错误", -1)
                    null
                }
            }
            //当协程取消的时候,取消网络请求
            deferred.invokeOnCompletion {
                if (deferred.isCancelled) {
                    call.cancel()
                    coroutine.clean()
                }
            }
            //await 等待异步执行的结果
            val response = deferred.await()
            if (response == null) {
                coroutine.onFail?.invoke("返回为空", -1)
            } else {
                response.let {
                    if (response.isSuccessful) {
                        //访问接口成功
                        if (response.body()?.status == 1) {
                            //判断status 为1 表示获取数据成功
                            coroutine.onSuccess?.invoke(response.body()!!.data)
                        } else {
                            coroutine.onFail?.invoke(response.body()?.msg ?: "返回数据为空", response.code())
                        }
                    } else {
                        coroutine.onFail?.invoke(response.errorBody().toString(), response.code())
                    }
                }
            }
            coroutine.onComplete?.invoke()
        }
    }}

在上面的代码中,比较难理解的是下面的代码:

val coroutine = RetrofitCoroutineDSL<T>().apply(dsl)

dsl是带接收者的函数字面量,接收者是RetrofitCoroutineDSL,所有先创建一个接受者对象,然后将传入的实参dsl赋值给该对象。还可以写成下面的样子:

val coroutine = RetrofitCoroutineDsl<T>()coroutine.dsl()

上面的写法是直接调用函数字面量。为了方便里面,把上述代码翻译成对应的Java代码:

RetrofitCoroutineDsl<T> coroutine = new RetrofitCoroutineDsl<T>();
dsl.invoke(coroutine);

调用函数dsl并传入coroutine,其实就是把dsl赋值给coroutine

3.最后一步,让BaseActivity实现接口CoroutineScope,这样在页面中的上下文就是协程下上文

open class BaseActivity : AppCompatActivity(), CoroutineScope {

    private lateinit var job: Job    override val coroutineContext: CoroutineContext        get() = Dispatchers.Main + job    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 关闭页面后,结束所有协程任务
        job.cancel() 
    }}

+CoroutineContext中的运算符重载,包含两者的上下文:

//Returns a context containing elements from this context and elements from  other [context].//The elements from this context with the same key as in the other one are dropped.public operator fun plus(context: CoroutineContext): CoroutineContext

Activity中可以直接调用扩展函数retrofit来调用网络请求:

retrofit<User> {
    api = RetrofitCreater.create(Api::class.java).login()
    onSuccess {
        Log.e(TAG, "result = ${it?.avatar}")
    }
    onFailed { msg, _ ->
        Log.e(TAG, "onFailed = $msg")
    }}

如果不需要处理访问失败的情况,可以写成下面的样子:

retrofit<User> {
    api = RetrofitCreater.create(Api::class.java).login()
    onSuccess {
        Log.e(TAG, "result = ${it?.avatar}")
    }}

使用协程可以更好的控制任务的执行,并且比线程更加的节省资源,更加的高效。结合DSL的代码风格,可以让我们的程序更加直观易懂、简洁优雅。

Kotlin协程原理详解

Kotlin DSL

Kotlin实战



  • 2019-12-06 10:47:29

    date-fns日期工具的使用方法详解

    isToday() 判断传入日期是否为今天 isYesterday() 判断传入日期是否为昨天 isTomorrow() 判断传入日期是否为 format() 日期格式化 addDays() 获得当前日期之后的日期 addHours() 获得当前时间n小时之后的时间点 addMinutes() 获得当前时间n分钟之后的时间 addMonths() 获得当前月之后n个月的月份 subDays() 获得当前时间之前n天的时间 subHours() 获得当前时间之前n小时的时间 subMinutes() 获得当前时间之前n分钟的时间 subMonths() 获得当前时间之前n个月的时间 differenceInYears() 获得两个时间相差的年份 differenceInWeeks() 获得两个时间相差的周数 differenceInDays() 获得两个时间相差的天数 differenceInHours() 获得两个时间相差的小时数 differenceInMinutes() 获得两个时间相差的分钟数

  • 2019-12-06 10:49:39

    npm 查看源 换源

    npm,cnpm,查看源,切换源,npm config set registry https://registry.npmjs.org

  • 2019-12-06 11:01:31

    npm发布包流程详解 有demo

    npm发布包步骤,以及踩过的坑(见红颜色标准): 1.注册npm账号,并完成Email认证(否则最后一步提交会报Email错误) 2.npm添加用户或登陆:npm adduser 或 npm login

  • 2019-12-06 13:16:18

    vue mixins组件复用的几种方式

    最近在做项目的时候,研究了mixins,此功能有妙处。用的时候有这样一个场景,页面的风格不同,但是执行的方法,和需要的数据非常的相似。我们是否要写两种组件呢?还是保留一个并且然后另个一并兼容另一个呢? 不管以上那种方式都不是很合理,因为组件写成2个,不仅麻烦而且维护麻烦;第二种虽然做了兼容但是页面逻辑造成混乱,必然不清晰;有没有好的方法,有那就是用vue的混合插件mixins。混合在Vue是为了提出相似的数据和功能,使代码易懂,简单、清晰。

  • 2019-12-06 13:26:30

    vue的mixins混入合并规则

    混入minxins:分发vue组件中可复用功能的灵活方式。混入对象可以包含任意组件选项。组件使用混入对象时,所有混入对象的选项将混入该组件本身的选项。

  • 2019-12-06 16:50:34

    Intellij idea 如何关闭无用的提示

    Linux:Settings —> Editor —> Inspections —> General —> Duplicated Code Mac:Preferences --> Editor —> Inspections —> General —> Duplicated Code fragment 将对应的勾去掉。

  • 2019-12-09 15:36:56

    神秘的 shadow-dom 浅析,shadow-root

    顾名思义, shadow-dom,直译的话就是 影子dom ?我觉得可以理解为潜藏在黑暗中的 DOM 结构,也就是我们无法直接控制操纵的 DOM 结构。前端同学经常用开发者工具的话,查看 DOM 结构的时候,肯定看到过下面这样的结构:

  • 2019-12-10 11:13:50

    前端实战-基于Nuxt的SVG使用

    虽然我们在日常开发的时候,在使用iview 或者element ui等组件时,通常会包含一些常用icon;但是在面对一些特定的需求时,或者自己想high一下,这些通用的icon并不能很好的满足我们。这个时候我们可能会拿到一些SVG适量图,但是怎么去使用这些矢量图呢。

  • 2019-12-10 11:15:08

    用CSS给SVG 的内容添加样式

    SVG图形的一个最常见用例是图标系统,其中最常用的SVG sprite技术就是使用SVG<use> 元素在文档中任意位置“实例化”图标。 使用<use>元素实例化图标或任何其它的SVG元素或图像,给元素添加样式时经常会碰到一些问题。这篇文章的目的是尽可能给你介绍一些方法来解决:使用<use>引入的内容添加样式受限的问题。 但是在开始之前,我们先快速浏览一下SVG的主要结构和分组元素,然后慢慢进入use的世界中,以及shadow DOM,然后重回CSS的怀抱。我们会逐步讲解为什么给<use>内容添加样式会比较麻烦,以及有什么好的解决方案。