简介

在项目中使用一段时间协程后,确实能体会到协程对于异步回调的简化,让我觉得最方便的是,作为调用方时无需关心应该使用哪个线程来执行函数,通常写函数时就可以定义好线程。

协程是Google官方推荐的Android异步编程解决方案,具有轻量,内存泄漏更少,内置取消支持,Jetpack集成等特点

在网上收集了一些对于协程的解释:

① 协程是轻量级线程

可以换个说法,协程就是方法调用封装成类线程的API。方法调用当然比线程切换轻量;而封装成类线程的API后,它形似线程(可手动启动、有各种运行状态、能够协作工作、能够并发执行)。因此从这个角度说,它是轻量级线程没错。

当然,协程绝不仅仅是方法调用,因为方法调用不能在一个方法执行到一半时挂起,之后又在原点恢复。这一点可以使用EventLoop之类的方式实现。想象一下在库级别将回调风格或Promise/Future风格的异步代码封装成同步风格,封装的结果就非常接近协程了。

② 线程运行在内核态,协程运行在用户态

主要明白什么叫用户态,我们写的几乎所有代码,都执行在用户态,协程对于操作系统来说仅仅是第三方提供的库而已,当然运行在用户态。而线程是操作系统级别的东西,运行在内核态。

③协程是一个线程框架

对某些语言,比如Kotlin,这样说是没有问题的,Kotlin的协程库可以指定协程运行的线程池,我们只需要操作协程,必要的线程切换操作交给库,从这个角度来说,协程就是一个线程框架。

但理论上我们可以在单线程语言如JavaScript、Python上实现协程(事实上他们已经实现了协程),这时我们再叫它线程框架可能就不合适了。

个人认为
以上几种说法都有其合理的支撑理由,在我个人的理解中,协程最重要的在于它的设计思想,基于线程实现了一种并发的设计模式,帮助开发者消除回调的代码

协程的使用

启动

协程需要运行在协程上下文环境,在非协程环境中凭空启动协程,有三种方式

  • runBlocking{}:启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。该方法的设计目的是让suspend风格编写的库能够在常规阻塞代码中使用,常在main方法和测试中使用
  • GlobalScope.launch{}:在应用范围内启动一个新协程,协程的生命周期与应用程序一致。这样启动的协程并不能使线程保活,就像守护线程。由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁创建销毁组件的场景。
  • 实现CoroutineScope + launch{}:这是在应用中最推荐使用的协程使用方式——为自己的组件实现CoroutieScope接口,在需要的地方使用launch{}方法启动协程。使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁。从而实现安全可靠地协程调用。

在一个协程中启动子协程:
launch{} 启动新协程而不将结果返回给调用方

async{} 启动一个新协程,并通过deferred的await方法暂停函数

取消

launch{}返回Job,async{}返回Deffer,Job和Deffer都有cancel()方法,用于取消协程。

协程的结构化并发,可以让协程非常便于管理。例如在关闭activity中要取消协程。如果是在线程中,取消所有的线程比较复杂。

取消父协程以及父里面的子协程

val scope = CoroutineScope(Dispatchers.Main+ Job())
        scope.launch {
            val job = launch {
               val job1 =  launch {
                   
                }
            }
            job.cancel()
        }
        scope.cancel()`

协程中异常处理

Kotlin协程的异常有两种

  • 因协程取消,协程内部suspend方法抛出的CancellationException
  • 常规异常,这类异常,有两种异常传播机制
    launch:将异常自动向父协程抛出,将会导致父协程退出
    async: 将异常暴露给用户(通过捕获deffer.await()抛出的异常)

在协程内部中捕获异常

val scope = CoroutineScope(Dispatchers.Main+ Job())
        scope.launch {
            try {
                
            }catch (e:Exception){
            }
        }

协程上下文

协程上下文表示协程的运行环境,包括协程调度器、代表协程本身的Job、协程名称、协程ID等。通过CoroutineContext定义,CoroutineContext是一个特殊的集合,这个集合它既有Map的特点,也有Set的特点,集合的每一个元素都是Element,每个Element都有一个Key与之对应。

  • Job:协程的唯一标识,用来控制协程的生命周期(new、active、completing、completed、cancelling、cancelled);
  • CoroutineDispatcher:指定协程运行的线程(IO、Default、Main、Unconfined);
  • CoroutineName: 指定协程的名称,默认为coroutine;
  • CoroutineExceptionHandler: 指定协程的异常处理器,用来处理未捕获的异常.

UML图:

android 利用协程发起网络请求 安卓 协程_kotlin


Job

Job也是上下文元素,它代表协程本身,通过CoroutineScope的扩展方法launch启动一个协程后,它会返回一个Job对象,它是协程的唯一标识,这个Job对象包含了这个协程任务的一系列状态。

  • 父Job退出,所有子job会马上退出
  • 子job抛出除CancellationException(意味着正常取消)意外的异常会导致父Job马上退出

类似Thread,一个Job可能存在多种状态(Completing只是一个内部状态,外部观察还是Active状态)

android 利用协程发起网络请求 安卓 协程_作用域_02


要区分是主动取消还是异常导致一个协程退出,可以getCancellationException()查看退出原因。

CoroutineDispatcher
CoroutineDispatcher调度器是协程上下文中众多元素中最重要的一个,通过CoroutineDispatcher定义,它控制了协程以何种策略分配到哪些线程上运行。这里介绍几种常见的调度器

  • Dispatcher.Default:默认调度器。它使用JVM的共享线程池,该调度器的最大并发度是CPU的核心数,默认为2
  • Dispatcher.Unconfined:非受限调度器,不给协程指定运行的线程,在第一次被挂起(suspend)之前,由启动协程的线程执行它,但被挂起后, 会由恢复协程的线程继续执行, 如果一个协程会被挂起多次, 那么每次被恢复后, 都有可能被不同线程继续执行
  • Dispathcer.IO:IO调度器,他将阻塞的IO任务分流到一个共享的线程池中,使得不阻塞当前线程。该线程池大小为环境变量kotlinx.coroutines.io.parallelism的值,默认是64或核心数的较大者。与Dispatcher.Default内部都是线程池实现,运行在共享线程池,因此使用withContext(Dispatchers.IO)创建新的协程不一定会导致线程的切换。
  • Dispathcer.Main:把协程运行在平台相关的只能操作UI对象的Main线程,所以它根据不同的平台有不同的实现
    kotlin/js:kotlin/js是kotlin对JavaScript的支持,提供了转换kotlin代码,kotlin标准库的能力,npm包管理能力,在kotlin/js上Dispatchers.Main等效于Dispatchers.Default;
    kotlin/native:kotlin/native是一种将kotlin代码编译为无需虚拟机就可运行的原生二进制文件的技术, 它的主要目的是允许对不需要或不可能使用虚拟机的平台进行编译,例如嵌入式设备或iOS,在kotlin/native上Dispatchers.Main等效于Dispatchers.Default;
    kotlin/JVM:kotlin/JVM就是需要虚拟机才能编译的平台,例如Android就是属于kotlin/JVM,对于kotlin/JVM我们需要引入对应的dispatcher,例如Android就需要引入kotlinx-coroutines-android库,它里面有Android对应的Dispatchers.Main实现,其实就是把任务通过Handler运行在Android的主线程.

CoroutineName

协程的名字,它的结构很简单, 我们平时开发一般是不会去指定一个CoroutineName的,因为CoroutineName只在kotlin的调试模式下才会被用的, 它在debug模式下被用于设置协程运行线程的名字
CoroutineExceptionHandler

CoroutineExceptionHandler就是协程的异常处理器,用来处理协程运行中未捕获的异常,每一个创建的协程默认都会有一个异常处理器,我们可以在启动协程时通过CoroutineContext指定我们自定义的异常处理器

CoroutineExceptionHandler只对launch方法启动的根协程有效,而对async启动的根协程无效,因为async启动的根协程默认会捕获所有未捕获异常并把它放在Deferred中,等到用户调用Deferred的await方法才抛出,需要我们调用Deferred的await方法时try catch

子协程抛出的未捕获异常会委托父协程的CoroutineExceptionHandler处理,子协程设置的CoroutineExceptionHandler永远不会生效

作用域

协程作用域——CoroutineScope,用于管理协程:

  • 启动协程的方式 - 它定义了launch、async、withContext等协程启动方法(以extention的方式),并在这些方法内定义了启动子协程时上下文的继承方式。
  • 管理协程生命周期 - 它定义了cancel()方法,用于取消当前作用域,同时取消作用域内所有协程。

使用建议

避免使用GlobalScope.launch

GlobalScope是实现了CoroutineScope的单例对象,含有一个空的上下文对象

// GlobalScope的定义
public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

这意味着它的生命周期与整个应用绑定,并且永远不会被主动取消。这样将导致代码运行在不受控的作用域。
应该将自己的组件实现CoroutineScope,并在组件销毁时调用作用域的cancel()方法。实现方式多使用委托。

将协程设为可取消
协程取消属于协作操作,也就是说,在协程的 Job 被取消后,相应协程在挂起或检查是否存在取消操作之前不会被取消。如果您在协程中执行阻塞操作,请确保相应协程是可取消的。

注入调度程序
在创建新协程或调用 withContext 时,直接调用Dispatchers ,勿保存变量对 Dispatchers 进行硬编码

挂起函数应该能够安全地从主线程调用
挂起函数应该是主线程安全的,这意味着,您可以安全地从主线程调用挂起函数。如果某个类在协程中执行长期运行的阻塞操作,那么该类负责使用 withContext 将执行操作移出主线程。
此模式可以提高应用的可伸缩性,因为调用挂起函数的类无需担心使用哪个 Dispatcher 来处理哪种类型的工作。该责任将由执行相关工作的类承担。

ViewModel 应创建协程
ViewModel 类应创建协程,而不是公开挂起函数来执行业务逻辑。在 viewModelScope 中启动协程,修改的数据与ViewModel生命周期同步,无需手动处理相关操作