kotlin协程学习小计

我们来把Kotlin协程这件事从头到尾聊透,保证用最通俗的大白话来讲解,让你不仅学会怎么用,还能理解它背后的门道。

可以把协程想象成一个超级轻量级的、可以随时"摸鱼"的任务。线程是公司里的正式员工,虽然能干很多活,但招聘和解雇(创建和销毁)的成本很高。而协程就像是你手头的具体工作(比如写一份PPT),你可以随时暂停去干别的事(被挂起),回头再接着写,而你的工位(线程)在此期间被其他人使用着 。

1. 协程的三大核心支柱

Kotlin协程的设计围绕着三个紧密相关的概念:挂起函数、协程作用域、协程上下文。理解它们,就掌握了协程的基石。

1.1 挂起函数 (suspend fun):魔法发生的地方

这是协程最核心的魔力所在。

  • 是什么? 一个被 suspend 关键字标记的函数。它代表这个函数可能会耗时 ,并且可以在不阻塞当前线程的情况下暂停执行,等结果准备好了再恢复 。
  • 有什么用? 让异步代码"同步化"。不用写一堆回调函数,代码逻辑看起来就像从上到下顺序执行一样。
  • 怎么工作的(原理简析)? 编译器在背后把挂起函数变成了一个"状态机"。每次函数可能暂停时,都会有一个对应的状态。当函数恢复时,会根据状态标签跳到上次执行的地方继续执行 。
kotlin 复制代码
suspend fun fetchUserData(): String {
    println("开始请求网络") // 运行在某个线程
    // withContext 是一个挂起函数,它会将协程切换到 IO 线程池
    // 当前协程在此处被挂起,但外面的线程可以去干别的活
    val result = withContext(Dispatchers.IO) {
        delay(1000) // 模拟网络请求,这是一个挂起函数
        return@withContext "User Data"
    }
    println("网络请求结束,拿到数据:$result") // 恢复执行,可能回到了原来的线程
    return result
}

1.2 协程作用域 (CoroutineScope):协程的"管辖区域"

  • 是什么? 可以理解为一个协程的管理员 。它定义了所有通过它启动的协程的生命周期边界。它最重要的职责是保证结构化并发
  • 结构化并发的好处:
    1. 取消传播:如果取消一个作用域,它内部所有正在运行的协程都会被取消。
    2. 等待完成:一个作用域会等待它内部所有的子协程都完成,自己才算完成。
    3. 异常传播:如果子协程出错了,它会通知其他兄弟协程或父协程。
  • 常见的作用域:
    • GlobalScope全局大喇叭 。生命周期与整个应用程序一样长,非常容易导致协程泄漏,几乎不推荐在实战中使用
    • runBlocking笨重的桥梁 。它会阻塞当前线程 直到内部所有协程执行完毕。通常只用在main函数测试或连接既有阻塞代码,正式业务代码中禁止使用
    • coroutineScope智能的挂起者 。它是一个挂起函数,会创建一个新的作用域,但不会阻塞线程,而是挂起自己,直到内部所有子协程完成。非常适合在挂起函数内进行并行分解任务 。
    • Android专属viewModelScope (在ViewModel中) 和 lifecycleScope (在Activity/Fragment中)。它们会自动绑定生命周期,当页面销毁时,自动取消所有协程,安全无泄漏

1.3 协程上下文 (CoroutineContext):协程的"背包"

  • 是什么? 一个类似于 Map 的数据结构,里面装着协程运行所需的各种配置信息 。这个背包里主要有四大件:
    1. Job (工作证) :协程的唯一标识和控制器。通过 Job,你可以管理协程的生命周期 :启动、取消、判断是否活跃等 。
      • launch 返回一个 Job 对象。
      • async 返回一个 Deferred 对象(Deferred 继承自 Job,多了一个 await() 方法用于获取结果) 。
    2. CoroutineDispatcher (调度器) :决定这个协程在哪条线程或哪个线程池上执行
      • Dispatchers.Main:主线程,用于UI操作。
      • Dispatchers.IO:IO线程池,用于网络、数据库、文件读写。
      • Dispatchers.Default:默认线程池,用于CPU密集型任务,如JSON解析、排序。
      • Dispatchers.Unconfined:不限派,在第一挂起点前运行在当前线程,之后由恢复它的线程决定,慎用
    3. CoroutineName (名字):给协程起个名,方便调试 。
    4. CoroutineExceptionHandler (异常处理器):处理未捕获的异常 。

2. 如何启动协程:两大核心构建器

你需要在一个 CoroutineScope 里,使用构建器来启动协程。

  • launch:点火起飞,不要结果

    • 当你只关心执行任务,而不需要任务返回任何值时使用。它返回一个 Job 对象,你可以用它来取消协程或等待它完成 。
  • async:点火起飞,等待结果

    • 当你需要执行一个任务,并且最终要拿到它的返回值时使用。它返回一个 Deferred 对象,通过调用它的 await() 方法来获取结果。async 常用于并发执行多个独立任务 。
kotlin 复制代码
// 假设这是在一个 ViewModel 或 lifecycleScope 里
viewModelScope.launch {
    // 启动一个不返回结果的协程
    val job = launch(Dispatchers.IO) {
        // 执行一些IO操作,比如写入日志
    }

    // 并发执行两个网络请求
    val userDeferred = async { fetchUser() }  // 自动继承上下文,通常在默认调度器
    val postsDeferred = async { fetchPosts() }

    // 在这里挂起,等待两个结果都准备好
    val user = userDeferred.await()
    val posts = postsDeferred.await()

    // 更新UI (此时已在主线程)
    showUserData(user, posts)
}

3. 协程的"花式操作"

3.1 并发与等待

  • join() :等待由 launch 启动的 Job 完成 。
  • await() :等待 async 启动的 Deferred 的结果 。
  • 组合并发 :利用 async 实现并发,再通过 await() 汇聚结果。注意 :不要在 async 后直接跟 await(),那样就变回串行了 。

3.2 协程的取消

取消是协作式的。意思是,协程代码必须自己配合检查是否被取消,否则无法被取消 。

  • 如何让协程可以被取消?
    1. 调用 yield()ensureActive() 来检查状态 。
    2. 检查 isActive 这个布尔值 。
    3. 使用所有 kotlinx.coroutines 包下的挂起函数(如 delaywithContext 等),它们都是可取消的。
  • 释放资源 :在 finally 块中释放资源。但如果 finally 中又调用了挂起函数,需要用 withContext(NonCancellable) { ... } 包裹 。

3.3 异常处理

  • 传播方式launch 将异常直接抛出 给父协程;async 将异常存储在 Deferred 对象里 ,直到 await() 调用时才抛出 。
  • CoroutineExceptionHandler:可以设置全局的"异常兜底方案",但通常作用域内的结构化并发已经能很好地处理异常传播 。
  • supervisorScope主管作用域 。它与 coroutineScope 类似,但最大的区别是:一个子协程失败了,不会影响其他兄弟子协程。非常适合处理多个互相独立的任务 。

4. 协程的高级数据流

4.1 热数据通道:Channel

  • 是什么? 类似于一个阻塞队列 。用于不同协程之间传递数据(Stream)。一个协程发送,一个协程接收 。

4.2 冷数据流:Flow

  • 是什么? 可以理解为异步版本的 Sequence (序列) 。它是一个冷流,即只有当你开始收集 (collect) 它的时候,里面的代码才会执行并发射数据 。
  • 有什么用? 非常适合处理从数据库或网络源源不断返回大量数据的场景,或者接收实时更新。
  • 关键操作符
    • 构建:flow { ... }flowOfasFlow
    • 中间操作:mapfiltercatch(捕获上游异常)。
    • 末端操作:collecttoListfirst

4.3 状态持有者:StateFlowSharedFlow

  • StateFlow状态容器 。它总是保存一个最新的值,并且只有值发生变化时才发射。非常适合替代 LiveData 在ViewModel中持有UI状态 。
  • SharedFlow事件广播器。可以配置缓存策略,用于发送事件,比如一次性提示、导航事件等(但要注意避免事件丢失或重复消费)。

5. 总结与实战示例

让我们用一个完整的Android ViewModel 示例来串起所有知识:

kotlin 复制代码
class MyViewModel : ViewModel() {

    // 1. UI状态,使用 StateFlow 持有,对外暴露不可变的 StateFlow
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState

    fun loadUserProfile(userId: String) {
        // 2. 在 viewModelScope 中启动协程,自动感知生命周期
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            try {
                // 3. 并发获取用户信息和好友列表
                val userProfile = coroutineScope { // 使用 coroutineScope 进行结构化并发
                    val userDeferred = async(Dispatchers.IO) { userRepository.fetchUser(userId) }
                    val friendsDeferred = async(Dispatchers.IO) { friendRepository.fetchFriends(userId) }

                    // 4. 挂起等待两个结果
                    UserProfile(
                        user = userDeferred.await(),
                        friends = friendsDeferred.await()
                    )
                }
                // 5. 更新状态(自动在主线程)
                _uiState.value = UiState.Success(userProfile)
            } catch (e: Exception) {
                // 6. 异常处理
                _uiState.value = UiState.Error(e.message)
            }
        }
    }

    // 密封类表示不同的UI状态
    sealed class UiState {
        object Loading : UiState()
        data class Success(val profile: UserProfile) : UiState()
        data class Error(val message: String?) : UiState()
    }
}

希望这份详解能帮你彻底打通Kotlin协程的任督二脉。从今天起,写异步代码,享受同步代码的丝滑吧!

相关推荐
Gary Studio1 小时前
Android AIDL HAL工程结构示例
android
y = xⁿ2 小时前
MySQL八股知识合集
android·mysql·adb
andr_gale2 小时前
04_rc文件语法规则
android·framework·aosp
祖国的好青年3 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴4 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭4 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首4 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil5 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙5 小时前
echarts,3d堆叠图
android·3d·echarts