Kotlin CoroutineScope解密

CoroutineScope 是什么

CoroutineScope一个装协程的篮子,通俗地讲:你需要一个篮子来装鸡蛋(协程),这个篮子就是 CoroutineScope。

三个核心:

  1. 装协程 ------所有通过 launchasync 启动的协程,都必须挂在一个 Scope 上

  2. 管生命周期 ------篮子扔了(cancel()),里面的鸡蛋全碎(协程全取消)

  3. 传环境------篮子自带配置(线程池、异常处理器等),里面的鸡蛋都按这个配置运行

    Kotlin 复制代码
    // 没有Scope,启动不了协程
    launch { } // ❌ 编译错误
    
    // 先造个篮子
    val scope = CoroutineScope(Dispatchers.Main)
    
    // 往篮子里放鸡蛋
    scope.launch { } // ✅ 可以跑了
    
    // 不要篮子了,鸡蛋全扔
    scope.cancel() // 所有协程取消

源码角度看Scope

先别急着写代码,我们看看官方是怎么定义的:

Kotlin 复制代码
public interface CoroutineScope {
    /**
     * 此作用域的上下文。
     * 上下文被作用域封装,并用于实现作为该作用域扩展的协程构建器。
     * 除了访问 [Job] 实例以进行高级用法外,一般不建议在任何目的下在普通代码中访问此属性。
     * 按照惯例,应该包含一个 [Job] 实例以强制结构化并发。
     */
    public val coroutineContext: CoroutineContext
}

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

就这? 是的,接口只有一个属性,工厂方法也只是帮你补了一个Job

这说明一个核心事实:Scope本身不执行任何协程,它只是Context的携带者 + 生命周期的边界标记

我们再看launch函数的定义:

Kotlin 复制代码
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
}

注意,launchCoroutineScope的扩展函数。这意味着:

  • 没有Scope,你就调不了launch

  • launch里的block接收者仍然是CoroutineScope(形成了嵌套Scope)

这个设计非常精妙:每个协程都有自己的Scope,父协程取消时,子协程通过这个继承的Scope感知到取消信号。

Scope最容易被误解的三个点

CoroutineScope ≠ CoroutineContext

很多初学者把这两个概念划等号。Scope是Context的容器,不是Context本身。

Kotlin 复制代码
// 这是什么操作?
val context = job + Dispatchers.IO
context.launch { } // ❌ 编译不过

val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch { } // ✅
对比维度 CoroutineScope CoroutineContext
一句话定义 装协程的篮子 篮子里的配置清单
本质 容器/作用域 配置集合/上下文
接口定义 interface CoroutineScope { val coroutineContext: CoroutineContext } interface CoroutineContext
能否启动协程 ✅ 能:scope.launch { } ❌ 不能:context.launch { } 编译错误
生命周期 有生命周期,可取消:scope.cancel() 无生命周期,不可取消
可变性 通常是单个实例,贯穿整个生命周期 可组合:Job() + Dispatchers.Main + CoroutineName("name")
包含关系 持有 CoroutineContext CoroutineScope 持有
职责 管理协程的边界和生命周期 定义协程的行为配置(线程、Job、异常处理器等)
类比 公司部门:有人、有预算、有负责人 员工手册:规定几点上班、去哪办公、找谁汇报
代码示例 val scope = CoroutineScope(Job() + Dispatchers.IO) scope.launch { /* 干活 */ } val ctx = Job() + Dispatchers.Main + CoroutineName("Loader") // 只是个配置,不能跑协程
能否独立存在 必须依赖 Context(接口强制要求) 可以独立存在,不依赖 Scope
多个实例关系 各自独立,通常不组合 可以组合:ctx1 + ctx2
作用范围 横向边界:一组协程的集合 纵向传递:协程内部的继承链
取消操作 scope.cancel() → 取消所有子协程 job.cancel() → 只取消单个 Job

Job必须单独管理,不要复用

这是一个非常隐蔽的坑:

Kotlin 复制代码
// 错误示范
val job = Job()
val scope1 = CoroutineScope(job + Dispatchers.Main)
val scope2 = CoroutineScope(job + Dispatchers.IO)

scope1.cancel() // job被cancel了
scope2.launch { } // ❌ 协程启动失败,因为job已取消

原则:每个Scope应该有自己的Job实例,不要跨Scope共享同一个Job。

cancel()会永久失效

Kotlin 复制代码
val scope = CoroutineScope(Job())
scope.launch { /* 任务1 */ }
scope.cancel()

// 想重启?
scope.launch { /* 任务2 */ } // ❌ 不会执行,Scope已经死了

cancel()的本质是取消了Scope持有的Job。一个被cancel的Scope无法再启动新协程,你需要重建Scope。

不同业务场景下的Scope选型

UI层:永远使用lifecycleScope和viewModelScope

这是Google帮我们封装好的,生命周期绑定,开箱即用:

Kotlin 复制代码
class UserProfileActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 界面可见时执行
        lifecycleScope.launch {
            loadUserData()
        }
        
        // 仅在Resumed状态执行,自动暂停恢复
        lifecycleScope.launchWhenResumed {
            startAnimations()
        }
    }
}

class UserViewModel : ViewModel() {
    fun refresh() {
        viewModelScope.launch {
            repository.fetchData()
        }
    }
}

这里有个细节lifecycleScope默认使用的是Dispatchers.Main.immediate,所以在onCreate里直接launch,代码会立即执行,不会等下一个Looper循环。

数据层:Scope由调用方注入

数据层不应该自己创建Scope,这是分层架构的基本原则:

Kotlin 复制代码
// ❌ 错误:仓库自己管理协程生命周期
class UserRepository {
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    
    suspend fun getUser(): User {
        return scope.async {
            api.fetchUser()
        }.await() // 这写法太别扭了
    }
}

// ✅ 正确:挂起函数,Scope由上层决定
class UserRepository {
    suspend fun getUser(): User = withContext(Dispatchers.IO) {
        api.fetchUser()
    }
}

// 如果需要并行请求,用coroutineScope或 supervisorScope
suspend fun getUserWithFriends(): UserDetail {
    return coroutineScope {
        val userDeferred = async { api.fetchUser() }
        val friendsDeferred = async { api.fetchFriends() }
        UserDetail(userDeferred.await(), friendsDeferred.await())
    }
}

原则:挂起函数不感知Scope,并发组合用coroutineScope包装。

思考:为什么上面的写法是错误的呢?

并不是语法上的错误,而且会容易引起逻辑混乱, 不受控制

1. .await()挂起点,会挂起当前协程直到结果返回

但是 ------这个 async 运行在 scope 里,不是当前协程的 child!

复制代码
// 调用方
viewModelScope.launch {
    val user = repository.getUser() // ❌ 这里会挂起,但async里的协程不受viewModelScope控制
    updateUI(user)
}

// 如果用户退出界面,viewModelScope取消
// 但repository.getUser()里的async还在后台继续跑!内存泄漏 + 资源浪费
  1. 违背结构化并发原则

Repository 自己创建 Scope,意味着:

  • 这个 Scope 不依附于任何生命周期(Activity/ViewModel)

  • 调用者无法取消它

  • 即使整个应用退出,这个 Scope 可能还在跑

调试噩梦:

复制代码
// 在任意地方调用
repository.getUser() // 启动了一个“孤儿”协程

// 没有任何人能取消它!
// 没有取消句柄,没有生命周期感知

3.suspend 函数不应该是"启动器"

suspend fun 的语义是:我是挂起函数,我会在 当前协程 执行,不私自开线程

但这个函数:

  1. 私自创建 Scope

  2. 私自开新协程(async)

  3. 再等待它完成

这违反了 suspend 函数的基本契约。

后台服务类:自定义Scope + 手动释放

有些常驻对象需要长时间运行协程,比如传感器监听、WebSocket连接:

Kotlin 复制代码
class SensorMonitor(
    private val context: Context
) {
    // 使用SupervisorJob,一个子协程失败不影响其他
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Default + job)
    
    private var isActive = false
    
    fun start() {
        if (isActive) return
        isActive = true
        
        scope.launch {
            // 采集加速度传感器数据
            collectAccelerometer()
        }
        
        scope.launch {
            // 采集陀螺仪数据
            collectGyroscope()
        }
    }
    
    fun stop() {
        isActive = false
        job.cancelChildren() // 只取消子协程,保留Job活跃
        // 或者 job.cancel() 完全销毁Scope
    }
    
    fun release() {
        stop()
        job.cancel()
    }
}

这里用cancelChildren()而不是cancel(),是为了让Scope可以继续复用。

构建一个可重试的文件上传器

Kotlin 复制代码
package com.example.mytest0

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.util.Collections

class FileUploader(private val api :UploadService, private val externalScope : CoroutineScope) {
    // 外部传人scope,通常传viewModelScope或自定义Scope

    // 上传任务的Job,用于批量控制
    private val uploadJob = SupervisorJob()

    // 基于外部Scope创建子Scope,自动继承外部协程上下文
    private val uploadScope = CoroutineScope(externalScope.coroutineContext + uploadJob)

    // 当前正在上传的任务
    private val uploadingTasks = Collections.synchronizedSet(mutableSetOf<String>())

    private val maxRetries = 3

    /**
     * 上传文件,支持进度监听和自动重试
     */
    fun uploadFile(fileId : String, file : File, onProgress: (Float) -> Unit = {}, onResult : (Result<String>) -> Unit) {
        if (uploadingTasks.contains(fileId)) {
            onResult(Result.failure(IllegalStateException("文件正在上传")))
            return
        }

        uploadingTasks.add(fileId)
        uploadScope.launch {
            var retryCount = 0

            while (retryCount < maxRetries) {
                try {
                    val result = withContext(Dispatchers.IO) {
                        api.uploadFile(file) { percent ->
                            onProgress(percent)
                        }
                    }

                    withContext(Dispatchers.Main) {
                        onResult(Result.success(result))
                    }
                } catch (e : IOException) {
                    retryCount++
                    if (retryCount >= maxRetries) {
                        withContext(Dispatchers.Main) {
                            onResult(Result.failure(e))
                        }
                        break
                    }
                    delay(1000L * retryCount)
                }
            }
            uploadingTasks.remove(fileId)
        }
    }

    /**
     * 取消所有上传任务
     */
    fun cancelAll() {
        uploadJob.cancelChildren()
        uploadingTasks.clear()
    }

    /**
     * 释放资源,外部Scope销毁时自动调用
     */
    fun release() {
        uploadJob.cancel()
    }
}

class UploadService {
    suspend fun uploadFile(file : File, onProgress: (Float) -> Unit = {}) : String {
        delay(1000)
        return "success"
    }
}

使用方式:

Kotlin 复制代码
class UploadViewModel : ViewModel() {
    private val uploader = FileUploader(apiService, viewModelScope)
    
    fun startUpload(file: File) {
        uploader.uploadFile(
            fileId = file.name,
            file = file,
            onProgress = { progress ->
                _progress.value = progress
            },
            onResult = { result ->
                result.onSuccess { url ->
                    _uploadSuccess.value = url
                }
            }
        )
    }
    
    override fun onCleared() {
        uploader.release()
        super.onCleared()
    }
}

小结

1. Scope本质是凭证

没有Scope你启动不了协程,但它本身不干活。把它当成进入协程世界的门票。

2. 谁需要取消,谁管理Scope

UI组件管理自己的Scope,数据层不应该持有Scope,挂起函数就够了。

3. SupervisorJob是常备工具

大部分业务场景不需要子协程失败就取消父协程,SupervisorJob能避免很多非预期取消。

4. 自定义Scope三思而后行

先问自己:能不能用viewModelScope?能不能用coroutineScope?如果答案是否定的,再自定义。

5. 结构化并发不是束缚,是安全带

很多开发者觉得协程的限制太多,不如Thread灵活。但正是这些"限制"让你写出无泄漏、可取消、可测试的代码。

CoroutineScope, CoroutineContext, Job, Dispatcher之间的关系

CoroutineScope, CoroutineContext, Job, Dispatcher之间的关系我之间用相关的图进行说明一下,让大家更加容易理解。

以公司进行进行说明

层级关系树

生命周期时序图

一句话总结图

关系口诀:

Scope 抱着 Context,Context 装着 Job 和 Dispatcher
Scope 生协程,协程挂 Job 上,跑在 Dispatcher 里
爸爸取消,儿子全停;爸爸死了,绝后(不能再开新协程)

相关推荐
咩图1 小时前
VSCode+Python创建项目
开发语言·python
zhanglu51161 小时前
Java Lambda 表达式使用深度解析
开发语言·前端·python
Coding茶水间1 小时前
基于深度学习的车牌识别系统演示与介绍(YOLOv12/v11/v8/v5模型+Pyqt5界面+训练代码+数据集)
开发语言·人工智能·深度学习·yolo·机器学习
郁闷的网纹蟒2 小时前
虚幻5---第15部分---宝藏(掉落物)
开发语言·c++·ue5·游戏引擎·虚幻
遇雪长安2 小时前
高通安卓设备DIAG端口启用指南
android·adb·usb·dm·qpst·diag·qxdm
大鹏说大话2 小时前
深入理解 Go 中的 make(chan chan error):高阶通道的典型用法与实战场景
开发语言·后端·golang
yuuki2332332 小时前
【C++】模拟实现 红黑树(RBTree)
java·开发语言·c++
csbysj20202 小时前
Bootstrap4 卡片
开发语言
IvanCodes2 小时前
九、C语言动态内存管理
c语言·开发语言·算法