Kotlin 协程 - 协程作用域 CoroutineScope
是一个接口。用来追踪内部启动的协程,并对它们进行有组织的统一管理(结构化并发),因此不能在协程作用域外启动协程。例如当Activity销毁后,协程还在执行,此时会造成资源浪费(内存、CPU、磁盘、无用网络请求)。协程作用域被cancel()取消的话,里面的子协程都会被取消。...............................................................
一、概念
协程作用域 CoroutineScope 是一个接口,没有任何抽象方法需要实现,仅仅维护一个成员变量 CoroutineContext(协程上下文),将作为初始上下文对象传递给被创建的协程,不同的实现类或作用域函数本质上的区别是持有的协程上下文不同(配置不同)。
实现类 | 核心库 | GlobalScope |
工厂函数 | MainScope( ) CoroutineScope( ) | |
平台支持 | ViewModel.viewModelScope LifecycleOwner.lifecycleScope | |
扩展函数 | 协程构建器 | launch( ) async( ) |
作用域函数 | 普通函数 | runBlocking( ) |
挂起函数 | coroutineScope( ) supervisorScope( ) withContext( ) withTimeout( ) withTimeoutOrNull( ) |
二、普通函数 runBlocking()
会阻塞当前线程直到其内部所有协程执行完毕,内部的协程彼此之间依旧是非阻式。用于把阻塞式的普通函数内部改为协程式编写,由于会阻塞线程在开发中不会使用,一般用于main函数作测试,单元测试一般使用runTest。
public fun <T> runBlocking( context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T ): T |
fun main(): Unit = runBlocking {
launch { println("...") }
}
三、实现类
3.1 核心库
3.1.1 GlobalScope
单例对象,不推荐使用。全局协程作用域,不绑定到任何Job上无法取消,通过它启动的子协程不会阻塞其所在线程可以一直运行到APP停止(相当于守护线程不会阻止JVM结束运行),子协程运行在自己的调度器上不会继承上下文与父协程没有联系,因此所有开启的子协程都需要分别手动来管理(容易造成内存泄漏和CPU冗余使用,例如当Activity销毁后协程还在执行耗时操作占用资源)。
3.1.2 ContextScope
上下文作用域,intermal修饰未对外暴露,根据指定的上下文创建协程作用域。使用工厂函数 MainScope()、CoroutineScope() 传入上下文对象参数,获取到的就是 ContextScope 实例。
3.2 工厂函数
3.2.1 mainScope( )
默认上下文使用 SupervisorJob()+Dispatchers.Main 的协程作用域。该调度器会绑定到主线程(在Android中就是 UI Thread),在 onDestroy() 中调用 scope.cancel() 关闭协程。可用于主动控制协程的生命周期,对Android开发意义在于避免内存泄漏。
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main) |
class AndroidActivity{
private val scope = MianScope()
scope.launch{
launch{
coroutineContext //父协程的上下文
currentCoroutineContext() //当前协程的上下文
}
}
override fun onDestroy(){
scope.cancel() //手动在生命周期里释放资源
}
}
3.2.2 CoroutineScope( )
根据自定义上下文创建协程作用域(如果上下文中没有 Job 会自动创建一个用于结构化并发)。CoroutineScope是一个只包含 coroutineContext 属性的接口,虽然我们可以创建一个实现类但这不是一个流行的做法,而且存在不小心在其它地方取消作用域。通常我们会更喜欢通过对象来启动协程,最简单的办法是使用 CoroutineScope() 工厂函数,它用传入的上下文来创建作用域。
public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job()) |
3.3 Android平台支持
3.3.1 ViewModel.viewModelScope( )
是ViewModelKTX 提供的扩展属性。使用的上下文是SupervisorJob() + Dispatchers.Main.immediate,ViewModel销毁时协程作用域会自动被 cancel,避免造成协程泄漏(内存泄漏)。
public val ViewModel.viewModelScope: CoroutineScope |
3.3.2 LifecycleOwner.lifecycleScope( )
是 LifeCycleKTX 提供的扩展属性。使用的上下文是SupervisorJob() + Dispatchers.Main.immediate,在 Activity/Fragment 销毁时协程作用域会自动被 cancel,避免造成协程泄漏(内存泄漏)。
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope |
四、扩展函数
4.1 协程构建器 CoroutineBuilder
launch() 和 async() 是 CoroutineScope 接口的扩展函数,继承了它的 coutineContext 来自动传播其上下文元素和可取消性。挂起函数需要相互传递 Continuation,每个挂起函数都要由另一个挂起函数或协程调用,这一切都是从协程构建器创建协程开始的,即作用域函数只能创建子协程,协程构建器能创建根协程或子协程(因为它通过实例调用可以存在于普通函数中)。
挂起函数创建的子协程是串行运行,协程构建器创建的子协程是并行运行。
参数 context:指定协程上下文。默认为空的上下文。 参数 start:指定协程启动模式。默认为可以立刻被调度的状态。 参数 block:协程执行体,即要做的任务。 | |
launch( ) 无需产生值 | public fun CoroutineScope.launch( 返回一个Job实例用来管理协程的生命周期。 |
async( ) 需要产生值 | public fun <T> CoroutineScope.async( 返回一个Deferred实例(Job的子类),通过 await() 拿到执行的结果(包括异常)。由于 await() 是挂起函数只能在协程作用域中调用,因此不要用 async() 做根协程,拿不到值就相当于 launch()。通常用于在协程作用域中构建并发子协程合并结果。 |
4.1.1 协程启动模式 CoroutineStart
CoroutineStart.DEFAULT | 协程创建后立即开始调度(不一定此时就被线程执行了),在被线程执行前如果协程被取消,其将直接进入取消响应状态, |
CoroutineStart.LAZY | 只要协程被需要时(包括主动调用 start()、join()、await())才会开始调度。如果调度前被取消,协程将进入异常结束状态。 |
CoroutineStart.ATOMIC | 协程创建后立即开始调度,内部代码执行到第一个挂起点之前不响应取消操作(内部第一个挂起函数之前的代码一定执行)。 |
CoroutineStart.UNDISPATCHED | 协程被创建后立即在当前函数调用栈中执行(所处的函数在哪个线程就是哪个,即便该协程通过Dispatcher指定了运行的线程),直到内部代码执行到第一个挂起点,挂起函数运行完后,之后的代码就是在Dispatcher指定的线程中运行了。 |
//ATOMIC模式
val job = launch(start = CoroutineStart.ATOMIC) {
//这里的代码一定会执行
delay(10000) //第一个挂起点
println("Job 完成")
}
delay(1000)
job.cancel() //取消上面的job
println("main 结束")
//LAZY模式
val deferred = async(start = CoroutineStart.LAZY) {
27
}
//此处执行一些计算...之后才需要拿到 deferred的值
deferred.await() //如果在这之前调用cancel()取消,就直接抛异常JobCancellationException
//UNDISPATCHED模式
launch(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
println("挂起之前:" + Thread.currentThread().name) //打印:main
delay(10)
println("挂起之后:" + Thread.currentThread().name) //打印:DefaultDispatcher-worker-1
}
五、作用域函数
都是挂起函数不会阻塞线程。由于挂起需要协程环境,只能由其它挂起函数或构建器调用,因此只能用来创建子协程。形参上将Lambda的接收者指定为CoroutineScope,因此可以在内部调用构建器开启子协程,通常被用于包装函数(一个作用域包装一堆调用尤其是withContext() )。
异常结束自身 | supervisorScope( ) | ||
异常连锁反应 | 指定上下文 | withContext( ) | |
继承上下文 | coroutineScope( ) | ||
限制执行时间 | withTimeout( ) withTimeoutOrNull( ) | ||
注意:
|
suspend fun main() = runBlocking {
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
delay(1000)
println("协程1")
}
scope.launch {
delay(1000)
println("协程2")
}
// scope.cancel() //作用域被取消,里面的子协程都会被取消
//delay是因为scope作用域自定义了上下文(调度器),没有继承父协程runBlicking的。
//它指定在了其它线程执行,自己也没有挂起特性,主线程会发现没有任务了会直接结束。
//也说明了协程不会阻塞线程。
delay(2000)
}
5.1 coroutineScope( )
- 异常:会连锁取消子协程、兄弟协程和父协程。
- 上下文:继承上下文
- 使用场景:经常被用来包装一个挂起函数的主体。多用于并行分解任务逻辑。
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R |
suspend fun main() = runBlocking { //将main()函数内部改为支持协程式编写,内部全部子协程运行完才会退出
//使用coroutineScope会挂起父协程runBlocking,不然因为协程不会阻塞线程,runBlocking会直接结束
coroutineScope { //这里换成supervisorScope,子协程1就会执行
val job1 = launch {
delay(5000)
println("子协程1")
}
val job2 = launch {
delay(1000)
println("子协程2")
throw Exception() //子协程1不会执行
}
}
}
5.2 supervisorScope( )
- 异常:不会影响兄弟协程和父协程,相当于一个独立的根协程,只关注自己内部。
- 上下文:继承上下文
- 使用场景:主要用于启动多个独立任务。
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R |
5.3 withContext( )
- 异常:会连锁取消子协程、兄弟协程和父协程。
- 上下文:指定上下文
- 使用场景:经常用来指定协程执行的线程和启动模式。这样在封装函数的时候,里面的业务代码就会被执行在正确的线程中(如 CPU密集型计算、IO、更新UI)
public suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T ): T |
suspend fun getSum(num1: Int, num2: Int): Int = withContext(Despatcher.DEFAULT) {
num1 + num2
}
suspend fun getData(): UserBean = withContext(Despatcher.IO) {
Retrofit.apiService.getData()
}
5.4 withTimeout( )
- 异常:会连锁取消子协程、兄弟协程和父协程。
- 上下文:继承上下文
- 使用场景:超时未执行完会抛异常,并返回一个值。超时抛出的TimeoutCancellationException是CancellationException子类,因此不会影响其他协程。
public suspend fun <T> withTimeout( timeout: Duration, block: suspend CoroutineScope.() -> T ): T |
withTimeout(1000) {
println("")
}
5.5 withTimeoutOrNull( )
- 异常:子协程异常会连锁取消其它子协程和自己。
- 上下文:继承上下文
- 使用场景:超时未执行完不抛异常,返回null。用来包装那些出现异常后会一直等待的操作,例如网络操作等待结果超过5s后不太可能会收到结果了。
public suspend fun <T> withTimeoutOrNull( timeMillis: Long, block: suspend CoroutineScope.() -> T ): T? |
withTimeoutOrNull(1000) {
println("")
} ?: "值为空"
六、创建协程的区别
协程构建器和协程作用域函数中都包含了形参 block: suspend CoroutineScope.() -> Unit。在这个block里写代码就相当于在协程作用域的一个函数里写代码,因此我们可以通过属性coroutineContext拿到协程上下文,能调用协程构建器去创建子协程,也能调用挂起函数去挂起当前协程。
全局 | GlobalScope | |||
阻塞 | runBlocking( ) | |||
生命感知 | MainScope( ) ViewModel.viewModelScope LifecycleOwner.lifecycleScope | |||
自定义 | CoroutineScope( ) | |||
挂起 | 异常结束自身 | supervisorScope( ) | ||
异常连锁反应 | 指定上下文 | withContext( ) | ||
继承上下文 | coroutineScope( ) | |||
限制执行时间 | withTimeout( ) withTimeoutOrNull( ) |
通过 协程构建器 创建 | 通过 协程作用域函数 创建 |
①通过协程作用域实例调用,创建的可能是根协程(在普通函数中)也可能是子协程(在其它协程中); ②直接调用创建的只能是子协程(必须存在父协程,即外部有上面说的block包裹)。 | 创建的只能是子协程。挂起函数“挂起恢复”的特性只能在协程环境下实现,因此只能在其它挂起函数或协程中调用,一定存在父协程。 |
|
|
tihs是被创建出来的协程,因为调用block的是被创建出来的协程,协程都是 AbstractCoroutine 的子类,而它实现了CoroutineScope,并不是调用它的协程作用域对象。 | |
因此在block中创建子协程: ①通过协程构建器:能直接通过 coroutineScope 属性拿到当前协程的上下文继承,从而形成父子关系具备传播取消和异常。 ②通过作用域函数:父协程被挂起时会返回一个 Continuation 延续体给挂起函数,这个延续体就是父协程(协程都是 AbstractCoroutine 的子类,而它实现了Continuation),从而获取并继承上下文,形成父子关系。 ③通过协程作用域对象调用协程构建器:不是子协程,因为没有上下文继承关系,无法通过Job传播取消和异常。GlobalScope是EmptyCoroutineContext,而MainScop、ViewModelScope、LifecyleScope都是指定的SupervisorJob(),至于CoroutineScope()虽然构造可以传入父协程上下文构建内外相同的作用域,何必多此一举。 |
GlobalScope.launch {
//不是子协程,外层的取消不会取消它
CoroutineScope(Dispatchers.IO).launch { }
//子协程,虽然创建了新的作用域对象,但是通过coroutineContext获取了父协程上下文,间接继承
CoroutineScope(coroutineContext).launch { }
//不是子协程,Job会替换掉父协程中的Job
CoroutineScope(coroutineContext + Job()).launch { }
}
多任务-串行 | launch + 多个 withContext |
多任务-并行 | launch + 多个 async |
launch + 多个 launch |
七、多个协程作用域之间的关系
在处理过程中,需要执行一个额外的非必要操作(例如收集数据分析),若是存在于挂起函数中,则会额外等待这个操作执行完,若这个操作发生异常还会波及正常的业务,这个时候最好是放在一个单独的作用域里启动它。一般通过构造(单元测试、控制此作用域)或者函数注入,如果是为了调用一些函数使用SupervisorJob,如果是收集异常信息使用CoroutineExceptionHandler。
在结构化并发中,由于作用域存在嵌套使用,因此有多种情况:
类型 | 场景举例 | 异常传播特征 |
顶级作用域 | 1.根协程之间。2.GlobalScope嵌套GlobalScope彼此独立互不影响。3.A和B是两个作用域对象,A开启的作用域中B开启了作用域,两个作用域彼此独立互不影响。4.supervisorScope() 或 supervisorJob 由于使用了新的 Job,相当于是一个独立的根协程,与外部互不影响。 | 不向外传播。 |
协同作用域 | 外层有父协程,且自身非另外的作用域对象开启。 | 双向传播。 |
主从作用域 | 外层有父协程,自身是supervisorScope()或supervisorJob。与内部直接子协程主从,与外部协同。 | 向下单向传播。 |
八、其它函数/属性
8.1 cancel( )
函数一:public fun CoroutineScope.cancel(cause: CancellationException? = null) 函数二:public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Unit = cancel(CancellationException(message, cause)) 取消协程会抛异常,默认可空也可以自定义scope.cancel(CancellationException("取消")),作用域里的子协程会全部被取消。底层调用的是Job的取消。作用域的上下文中没有检测出包含Job会抛异常(例如GlobalScope的上下文是EmptyCoroutineContext就无法使用,但是它调用launch()后返回的Job可以使用)。一般用于有生命周期的组件中释放资源避免内存泄漏。 |
九、Android开发
直接使用 viewModelScope() 和 lifecycleScope(),以下是手写的情况。
- 定义在基类中:通常会在 ViewModel 中启动协程,在其它层例如仓库只创建挂起函数。
- 上下文:
- 调度器:Android中默认为主线程,因此选择 Dispatcher.Main.immediate。
- Job:不能因为子协程异常而连带取消其它协程,例如用户信息加载出错影响动态列表功能,因此选择 SupervisorJob。
- 生命周期:
- 方式一:在 ViewModel 的 onCleared() 中关闭协程作用域。
- 方式二:不取消整个作用域而是取消子协程,因此只要 ViewModel 存活,就可以继续启动新的协程。
- 异常:
- 方式一:在 BaseActivity 中写下异常处理方法,通过构造传递给 ViewModel。有未处理的异常就可以用 CoroutineExceptionHandler 里调用函数。
- 方式二:使用 Livedata 保存。有未处理的异常就可以在 CoroutineExceptionHandler 里对 LiveData 赋值,在其它地方观察处理。
abstract class BaseViewModel(
private val onError: (Throwable) -> Unit
) : ViewModel() {
private val context = Dispatchers.Main.immediate + SupervisorJob() + exceptionHandler
protected val scope = CoroutineScope(context)
private val failure: MutableLiveData<Throwable> = MutableLiveData()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError(throwable)
failure.value = throwable
}
override fun onCleared() {
//scope.coroutineContext.cancelChildren()
context.cancelChildren()
}
}
![Logo](https://devpress.csdnimg.cn/79de2bf0b7994defa4242ef90d5513fa.jpg)
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)