Kotlin协程笔记

概述

Kotlin中,协程是一套由Kotlin官方提供的线程API
它有优点:

  • 可以通过看似同步的方式写出异步代码。这也是协程的特性之一 非阻塞式挂起
  • 可以解决地狱回调的问题。

例如,如下代码形式

launch({
    val user = api.getUser() // 👈 网络请求(IO 线程)
    nameTv.text = user.name  // 👈 更新 UI(主线程)
})

协程的使用

在需要切换线程或者指定线程的时候 用到协程。

协程的启动

最基本用法。launch 函数加上实现在 {} 中具体的逻辑,就构成了一个协程。

有以下3种启动方式

  • runBlocking 启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T,就是你协程体中最后一行是什么类型,最终返回的是什么类型T就是什么类型。很少用,一般用于测试协程。
  • scope.launch() 启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用,返回值是一个Job
    它具体的含义是:我要创建一个新的协程,并在指定的线程上运行它。这个被创建、被运行的所谓「协程」是谁?就是你传给 launch 的那些代码,这一段连续代码叫做一个「协程」。
  • scope.async() 启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用。以Deferred对象的形式返回协程任务。返回值泛型TrunBlocking类似都是协程体最后一行的类型。
    返回的都是 Coroutine.不同点:async 返回的 Coroutine 多实现了 Deferred 接口。它的意思就是延迟,也就是结果稍后才能拿到。我们调用 Deferred.await() 就可以得到结果了。

协程的作用域 CoroutineScope

协程作用域(Coroutine Scope)是协程运行的作用范围。launchasync都是CoroutineScope的扩展函数,CoroutineScope定义了新启动的协程作用范围,同时会继承了他的coroutineContext自动传播其所有的 elements和取消操作。

协程的调度器 CoroutineDispatcher

  • Default:默认调度器,CPU密集型任务调度器,适合处理后台计算。通常处理一些单纯的计算任务,或者执行时间较短任务。比如:Json的解析,数据计算等
  • IO:IO调度器,,IO密集型任务调度器,适合执行IO相关操作。比如:网络处理,数据库操作,文件操作等
  • Main:UI调度器, 即在主线程上执行,通常用于UI交互,刷新等
  • Unconfined:非受限调度器,又或者称为“无所谓”调度器,不要求协程执行在特定线程上。

在作用域中切换线程。官方为我们提供了一个withContext顶级函数,使用withContext函数来改变协程的上下文,而仍然驻留在相同的协程中,同时withContext还携带有一个泛型T返回值。

withContext 使用场景演示

// 第一种写法
coroutineScope.launch(Dispatchers.IO) {
    ...
    launch(Dispatchers.Main){
        ...
        launch(Dispatchers.IO) {
            ...
            launch(Dispatchers.Main) {
                ...
            }
        }
    }
}

// 通过第二种写法来实现相同的逻辑
coroutineScope.launch(Dispatchers.Main) {
    ...
    withContext(Dispatchers.IO) {
        ...
    }
    ...
    withContext(Dispatchers.IO) {
        ...
    }
    ...
}

async使用场景演示

coroutineScope.launch(Dispatchers.Main) {
    //                      👇  async 函数启动新的协程
    val avatar: Deferred = async { api.getAvatar(user) }    // 获取用户头像
    val logo: Deferred = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
    //            👇          👇 获取返回值
    show(avatar.await(), logo.await())                     // 更新 UI
}

协程上下文 CoroutineContext

CoroutineContext即协程上下文。它是一个包含了用户定义的一些各种不同元素的Element对象集合。其中主要元素是Job、协程调度器CoroutineDispatcher、还有包含协程异常CoroutineExceptionHandler、拦截器ContinuationInterceptor、协程名CoroutineName等。这些数据都是和协程密切相关的,每一个Element都一个唯一key

CoroutineContext 有以下主要方法

public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E?
    
public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R
    
public operator fun plus(context: CoroutineContext): CoroutineContext =
    if (context === EmptyCoroutineContext) this else context.fold(this) { ...}
        
public fun minusKey(key: Key<*>): CoroutineContext

plus方法, plus有个关键字operator表示这是一个运算符重载的方法,类似List.plus的运算符,可以通过+号来返回一个包含原始集合和第二个操作数中的元素的结果。同理CoroutineContext中是通过plus来返回一个由原始的Element集合和通过+号引入的Element产生新的Element集合。
get方法,顾名思义。可以通过 key 来获取一个Element
fold方法它和集合中的fold是一样的,用来遍历当前协程上下文中的Element集合。
minusKey方法与plus作用相反,它相当于是做减法,是用来取出除key以外的当前协程上下文其他Element,返回的就是不包含key的协程上下文。
我们就是通过key从协程上下文中获取我们想要的Element,同时也解释为什么JobCoroutineDispatcherCoroutineExceptionHandlerContinuationInterceptorCoroutineName等等,这些Element都有需要有一个CoroutineContext.Key类型的伴生对象key。

但是因为这个+运算符是不对称的,所以在我们实际的运用过程中,通过+增加Element的时候一定要注意它们结合的顺序。新增加同一类型会覆盖之前的。

//异常处理
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    Log.d("exceptionHandler", "${coroutineContext[CoroutineName]}$throwable")
}
//自定义作用域
val commentJob = Job()
val commentScope = CoroutineScope(Dispatchers.Main + commentJob + exceptionHander)
//simpleRequestData 为 `CoroutineScope`的扩展函数
commentScope.simpleRequestData<ArrayList<CollectionPointModel>>()

android中的使用

  1. GlobalScope.launch 如果不指定Dispatchers的话,默认执行在Default(一个计算线程).不过,作为全局Scope,执行任务是不可以取消的(不可以调用scope.cancel() 否则会抛出严重的异常)。
  2. MainScope 特点 SupervisorJob() + Dispatchers.Main
  3. android ViewModule下的 viewModelScope,默认执行在 主线程
  4. activity fragmentlifecycleScope跟生命周期关联。lifecycle状态处于DESTROYED状态的时候自动关闭所有的协程。
    同时我们也可以通过launchWhenCreatedlaunchWhenStartedlaunchWhenResumed来启动协程,等到lifecycle处于对应状态时自动触发此处创建的协程。
  5. 自定义CoroutineScope. 例如 val normalScope = ContextScope(SupervisorJob() + Dispatchers.Main)

操作函数

launchasyncCoroutineScope下的函数,不可以单独使用。
coroutineScopewithContext可单独使用。

launch
launch 常用函数,创建一个coroutine并不阻塞当前线程,返回。

//launch函数的定义,
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

async
async 创建一个coroutine,返回值可立即获得,拿到这个返回值需要等任务执行完。

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

coroutineScope
coroutineScope 创建一个coroutine,由制定的scope执行,这个scope同外部scope,也就是调用该函数外部scope.

常用于定义协程函数。

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R

使用

private suspend fun handleException(
        block: suspend CoroutineScope.() -> Unit,
        error: suspend CoroutineScope.(ResponseThrowable) -> Unit,
        complete: suspend CoroutineScope.() -> Unit
    ) {
        coroutineScope {
            try {
                block()
            } catch (e: Throwable) {
      			//异常的处理逻辑
                error(ExceptionHandle.handleException(e))
            } finally {
                complete()
            }
        }
    }

withContext
withContext 由制定的coroutine执行,挂起,结束返回。

常用于内部切换执行线程。

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T 

关于挂起

suspend关键字理解。它其实是一个提醒。函数的创建者对函数的使用者的提醒:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,所以请在协程里调用我。

挂起函数在执行完成之后,协程会重新切回它原先的线程。
再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作。

参考资料

史上最详Android版kotlin协程入门进阶实战
扔物线协程教程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值