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-10 16:21:05

    display:flex的子元素无法设置宽度

    子元素有个flex-shrink属性,表示在父元素宽度不够的情况下是自动收缩不?0表示不自动收缩,1表示自动收缩;所以将子元素(图片)添加属性:flex-shrink:0;即

  • 2019-12-10 21:14:11

    axios文件上传功能+formData

    在项目中使用axios上传文件,记得new一个纯净的axios或者考虑用ajax请求。因为axios在项目估计已经用了全局配置请求头等信息,这里的配置可能被全局请求头拦截,导致请求失败。 2.1构造formData 作者:exmexm 链接:https://www.jianshu.com/p/9c708a47d8a5 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 2019-12-11 16:04:15

    CSS中的 “var()” 和 “:root”

    var() var()函数可以代替元素中任何属性中的值的任何部分。var()函数不能作为属性名、选择器或者其他除了属性值之外的值。(这样做通常会产生无效的语法或者一个没有关联到变量的值。)

  • 2019-12-11 16:18:51

    npm发布vue组件

    开发之前先看看官网的 开发规范 我们开发的之后期望的结果是支持 import、require 或者直接使用 script 标签的形式引入,就像这样

  • 2019-12-11 16:21:00

    .vue文件 加scoped 样式不起作用

    在vue组件中,为了使样式私有化(模块化),不对全局造成污染,在style标签上添加scoped属性,以表示它只属于当下的模块。但是要慎用,因为在我们需要修改公共组件(第三方库或者项目中定制的组件)的样式的时候,scoped会造成很多困难,组要增加额外的复杂度。

  • 2019-12-11 16:22:04

    Vue中的scoped和scoped穿透,scoped原理

    在Vue文件中的style标签上有一个特殊的属性,scoped。当一个style标签拥有scoped属性时候,它的css样式只能用于当前的Vue组件,可以使组件的样式不相互污染。如果一个项目的所有style标签都加上了scoped属性,相当于实现了样式的模块化。

  • 2019-12-12 14:19:32

    laravel自定义分页LengthAwarePaginator

    有时候我们使用larave提供的后台分页数据库查询,有时候限制太多,我们需要自己定制分页功能。 下面是我给大家一个例子,我们可以根据例子,制作自己的分页功能。