当前位置: 首页 > news >正文

kotlin知识体系(五) :Android 协程全解析,从作用域到异常处理的全面指南

1. 什么是协程

协程(Coroutine)是轻量级的线程,支持挂起和恢复,从而避免阻塞线程。

在这里插入图片描述

2. 协程的优势

协程通过结构化并发和简洁的语法,显著提升了异步编程的效率与代码质量。

2.1 资源占用低(一个线程可运行多个协程)

传统多线程模型中,每个线程需要独立的系统资源(如内存栈),而协程共享线程资源。

  • 高效线程利用​​:通过调度器(如 Dispatchers.IO),一个线程池可同时处理数千个协程任务(如并发网络请求或文件读写)。
  • 减少上下文切换​​:协程挂起时不会阻塞线程,线程可立即执行其他协程任务,减少线程切换的性能损耗。

2.2 代码可读性强(顺序编写异步逻辑)

协程通过同步代码风格实现异步逻辑,彻底消除“回调地狱”。

  • 同步化表达:使用挂起函数(如 withContext、await())可将异步操作写成顺序执行的代码。
  • ​​结构化并发​​:通过 CoroutineScope 管理协程生命周期,自动取消子协程,避免内存泄漏。

3. 协程的核心组件

协程通过一组核心组件实现结构化并发和高效的任务管理。

3.1 ​​CoroutineScope(作用域)​

  • 管理协程的生命周期,确保协程在特定范围内启动和取消。
  • 通过结构化并发避免资源泄漏。
3.1.1 常见作用域​​:
  • lifecycleScope : 与 Lifecycle(如 Activity/Fragment)绑定,界面销毁时自动取消所有子协程。
  • viewModelScope​ : 与 ViewModel 绑定,ViewModel 销毁时自动取消协程,适合处理业务逻辑。

3.2 ​​CoroutineContext(上下文)​

定义协程的上下文信息,如线程调度器、协程名称、异常处理器等。

  • Job : 控制协程的生命周期(启动、取消、监控状态)
  • ​​Dispatcher​ : 指定协程运行的线程
  • CoroutineName : 为协程命名,便于调试

3.3 Dispatcher (调度器)

指定协程运行的线程

  • Dispatchers.Main​​ : 主线程,用于更新 UI 或执行轻量级操作。
    ​​注意​​:在非 Android 环境(如单元测试)中可能不存在。​​
  • Dispatchers.IO​​ : 适用于 IO 密集型任务(如网络请求、数据库读写、文件操作)。
    ​​底层机制​​:共享线程池,默认最小 64 线程。
  • Dispatchers.Default​​ : 适用于 CPU 密集型任务(如排序、计算、图像处理)。
    ​​底层机制​​:线程数与 CPU 核心数相同。​

3.4 ​​Job (作业)

3.4.1 Job (作业)

表示一个协程任务,不返回结果,通过 launch 创建。

val job = launch { /* ... */ }
job.start()    // 启动(默认自动启动)
job.cancel()   // 取消
job.join()     // 挂起当前协程,等待此 Job 完成
3.4.2 Deferred (异步结果)

Job 的子类,表示一个会返回结果的异步任务,通过 async 创建。

val deferred = async { fetchData() }  
val data = deferred.await() // 挂起协程直到结果就绪

4. 协程构建器

协程构建器是创建和启动协程的入口点,不同构建器适用于不同场景。

4.1 ​​launch:启动一个不返回结果的协程

启动一个不返回结果的协程,适用于“触发后无需等待结果”的任务(如日志上报、缓存清理)。

​特性

  • 返回 Job 对象,用于控制协程生命周期(取消、监控状态)。
  • 默认继承父协程的上下文(如作用域、调度器)。
// 在 ViewModel 中启动一个后台任务
fun startBackgroundTask() {viewModelScope.launch(Dispatchers.IO) {  cleanCache()    // 在 IO 线程执行清理操作log("Cache cleaned") // 完成后记录日志}// 无需等待结果,直接执行后续代码
}

4.2 ​​async:并发执行并获取结果​

启动一个返回结果的协程,适用于需要并行执行多个任务并汇总结果的场景。

特性​​:

  • 返回 Deferred 对象,通过 await() 挂起并获取结果。
  • 可通过 async 启动多个协程后统一等待结果,提升执行效率。

示例​​:并行请求多个接口并合并数据

viewModelScope.launch {// 同时发起两个网络请求val userDeferred = async(Dispatchers.IO) { fetchUser() }  val postsDeferred = async(Dispatchers.IO) { fetchPosts() }// 等待两个请求完成(总耗时取决于最慢的任务)val user = userDeferred.await()  val posts = postsDeferred.await()// 合并结果并更新 UIshowUserProfile(user, posts)
}

4.3 ​​runBlocking:在阻塞代码中启动协程​

阻塞当前线程,直到其内部的协程执行完毕。

​​主要用于测试​​,或在非协程环境中临时调用挂起函数。

示例​​:在单元测试中测试协程逻辑

@Test
fun testFetchData() = runBlocking {  // 阻塞当前线程,等待协程完成val data = fetchData() // 直接调用挂起函数assertEquals(expectedData, data)
}

应避免在主线程使用 runBlocking,因为会阻塞主线程 !

5. 挂起函数

挂起函数(Suspending Function)是协程的核心特性之一,允许协程在非阻塞的前提下暂停和恢复执行。挂起函数只能在协程或其他挂起函数中调用,适用于需要等待异步操作完成的场景。

5.1 ​​delay():协程的“非阻塞休眠”​

delay() 会暂停协程的执行指定时间(单位:毫秒),期间不会阻塞线程,线程可执行其他任务。

5.1.1 与 Thread.sleep() 的区别​​:
delay()Thread.sleep()
挂起协程,释放线程资源阻塞线程,线程无法执行其他任务
只能在协程或挂起函数中调用可在任何线程中调用
viewModelScope.launch {repeat(10) {  delay(1000)       // 每隔 1 秒执行一次,不阻塞主线程  updateCounter(it)  }  
}  

5.2 ​​withContext():灵活的线程切换​

在指定协程上下文(如 Dispatcher)中执行代码块,完成后自动恢复原上下文。替代传统回调或 Handler,简化线程切换逻辑。

suspend fun loadData() {  // 在 IO 线程执行网络请求  val data = withContext(Dispatchers.IO) {  api.fetchData()  }  // 自动切回调用方的上下文(如 Main 线程)  updateUI(data)  
}  
5.2.1 与 async 的区别​
  • withContext:直接返回结果,适用于单次切换线程的串行任务。
  • async:返回 Deferred,适用于并行任务。

避免嵌套多层 withContext,可用 async 替代以提升并发效率。

5.3 await():安全获取异步结果​

挂起协程,等待 Deferred 任务完成并返回结果。若 Deferred 任务出现异常,await() 会抛出该异常。

5.3.1 示例​​:并行任务与结果合并
viewModelScope.launch {  val task1 = async(Dispatchers.IO) { fetchDataA() }  val task2 = async(Dispatchers.IO) { fetchDataB() }  // 同时等待两个任务完成  val combinedData = combineData(task1.await(), task2.await())  
}  
5.3.2 await()怎么处理异常

使用 try-catch 捕获 await() 的异常:

val deferred = async { /* 可能抛出异常的代码 */ }  
try {  val result = deferred.await()  
} catch (e: Exception) {  handleError(e)  
}  

若需取消任务,调用 deferred.cancel()

5.3.3 协程是否存活

通过 coroutineContext.isActive 检查协程是否存活,可以及时终止无效操作

suspend fun heavyCalculation() {  withContext(Dispatchers.Default) {  for (i in 0..100000) {  if (!isActive) return@withContext // 检查协程是否被取消  // 执行计算  }  }  
}  

6. 协程的异常处理机制

6.1 异常传播机制

默认规则​​:

  • 子协程异常会向上传播​​:当子协程抛出未捕获的异常时,父协程会立即取消,进而取消所有其他子协程。
  • 兄弟协程受影响​​:若子协程 A 抛出异常,其兄弟协程 B 也会被取消,即使 B 仍在执行中。

示例​​:未捕获异常导致父协程取消

viewModelScope.launch {// 子协程 1launch {delay(100)throw IOException("网络请求失败") // 未捕获异常}// 子协程 2(会被父协程取消)launch {repeat(10) {delay(200)log("子任务执行中") // 仅执行 1 次后父协程取消}}
}

6.2 捕获异常的方式

6.2.1 方式 1:try-catch 块

在协程内部直接捕获异常,适用于同步代码逻辑。

viewModelScope.launch {try {fetchData() // 可能抛出异常的挂起函数} catch (e: IOException) {showError("网络异常: ${e.message}")}
}
6.2.2 方式 2:CoroutineExceptionHandler

全局异常处理器,用于捕获未通过 try-catch 处理的异常。

定义异常处理器

val exceptionHandler = CoroutineExceptionHandler { _, throwable ->log("未捕获异常: ${throwable.message}")showErrorToast() // 例如弹出 Toast
}

附加到协程上下文

viewModelScope.launch(exceptionHandler) {  launch { throw IOException() } // 异常会被 exceptionHandler 捕获
}

仅在根协程(直接通过 launch 或 async 创建的顶层协程)中生效。

6.3 隔离异常:SupervisorJob

阻止子协程的异常传播到父协程,避免“一颗老鼠屎坏了一锅粥”。常用于独立任务场景(如同时发起多个不相关的网络请求)。

  • 子协程的失败不会影响其他子协程。
  • 父协程仍会等待所有子协程完成(除非显式取消)。
6.3.1 ​​通过 SupervisorJob() 创建作用域​​
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch { throw Exception() } // 不影响其他子协程
scope.launch { delay(1000) }       // 正常执行
6.3.2 在现有作用域中使用 supervisorScope
viewModelScope.launch {supervisorScope {launch { throw IOException() } // 仅自身失败launch { delay(1000) }          // 继续执行}
}

6.4 自定义异常处理策略

可以根据业务需求设计容错逻辑,例如:

​​- 重试机制​​:在捕获异常后自动重试任务。
​​- 回退操作​​:失败时返回默认值或缓存数据。

6.4.1 示例:网络请求重试​
suspend fun fetchDataWithRetry(retries: Int = 3): Data {repeat(retries) { attempt ->try {return api.fetchData()} catch (e: IOException) {if (attempt == retries - 1) throw e // 最后一次重试仍失败则抛出异常delay(1000 * (attempt + 1))         // 延迟后重试(指数退避)}}throw IllegalStateException("Unreachable")
}

6.5 协程异常的最佳实践

6.5.1 明确异常边界​​
  • 在协程根节点或关键入口处统一处理异常(如使用 CoroutineExceptionHandler)。
  • 避免在底层函数中静默吞没异常(如 catch 后不处理)。
6.5.2 区分取消与异常
  • 使用 isActive 检查协程状态,及时终止无效任务。
  • 通过 ensureActive() 快速失败
public fun Job.ensureActive(): Unit {if (!isActive) throw getCancellationException()
}
6.5.3 ​​谨慎使用 SupervisorJob

仅当子协程完全独立时使用,避免隐藏潜在问题。

相关文章:

  • 深入理解组合实体模式(Composite Entity Pattern)在 C# 中的应用与实现
  • 基于SpringAI Alibaba实现RAG架构的深度解析与实践指南
  • 【数据结构_12】二叉树(4)
  • C 语言的未来:在变革中坚守与前行
  • Windows串口通信
  • 进程管理,关闭进程
  • PCA——主成分分析数学原理及代码
  • 【图像处理基石】什么是去马赛克算法?
  • springboot+vue3+mysql+websocket实现的即时通讯软件
  • 热门算法面试题第19天|Leetcode39. 组合总和40.组合总和II131.分割回文串
  • PyTorch基础笔记
  • 【笔记】SpringBoot实现图片上传和获取图片接口
  • MAC-从es中抽取数据存入表中怎么实现
  • 23种设计模式-结构型模式之适配器模式(Java版本)
  • 23种设计模式-结构型模式之装饰器模式(Java版本)
  • 延长(暂停)Windows更新
  • 学习设计模式《四》——单例模式
  • Halcon应用:相机标定
  • Deepseek输出的内容如何直接转化为word文件?
  • 大模型面经 | 介绍一下CLIP和BLIP
  • 第1现场|俄乌互指对方违反复活节临时停火提议
  • 谷雨播种正当时,上海黄道婆纪念公园种下“都市棉田”
  • 俄罗斯与乌克兰互换246名在押人员
  • 全国首家由司法行政部门赋码登记的商事调解组织落户上海
  • 观察|智驾监管升级挤掉宣传水分,行业或加速驶入安全快车道
  • 礼来公布口服降糖药积极结果,或年底前提交用于体重管理上市申请