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

news2025/4/21 20:08:30

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()
    
    // 合并结果并更新 UI
    showUserProfile(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 {
    // 子协程 1
    launch {
        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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2339645.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vscode stm32 variable uint32_t is not a type name 问题修复

问题 在使用vscodekeil开发stm32程序时,发现有时候vscode的自动补全功能失效,且problem窗口一直在报错。variable “uint32_t” is not a type name uint32_t 定义位置 uint32_t 实际是在D:/Keil_v5/ARM/ARMCC/include/stdint.h中定义的。将D:/Keil_v5…

Formality:Bug记录

相关阅读 Formalityhttps://blog.csdn.net/weixin_45791458/category_12841971.html?spm1001.2014.3001.5482 本文记录博主在使用Synopsys的形式验证工具Formality中遇到的一个Bug。 Bug复现 情况一 // 例1 module dff (input clk, input d_in, output d_out …

【java+Mysql】学生信息管理系统

学生信息管理系统是一种用于管理学生信息的软件系统,旨在提高学校管理效率和服务质量。本课程设计报告旨在介绍设计和实现学生信息管理系统的过程。报告首先分析了系统的需求,包括学生基本信息管理、成绩管理等功能。接着介绍了系统的设计方案&#xff0…

小白从0学习网站搭建的关键事项和避坑指南(2)

以下是针对小白从零学习网站搭建的 进阶注意事项和避坑指南(第二期),覆盖开发中的高阶技巧、常见陷阱及解决方案,帮助你在实战中提升效率和质量: 一、进阶技术选型避坑 1. 前端框架选择 误区:盲目追求最新…

Windows 10 上安装 Spring Boot CLI详细步骤

在 Windows 10 上安装 Spring Boot CLI 可以通过以下几种方式完成。以下是详细的步骤说明: 1. 手动安装(推荐) 步骤 1:下载 Spring Boot CLI 访问 Spring Boot CLI 官方发布页面。下载最新版本的 .zip 文件(例如 sp…

vue2技术练习-开发了一个宠物相关的前端静态商城网站-宠物商城网站

为了尽快学习掌握相关的前端技术,最近又实用 vue2做了一个宠物行业的前端静态网站商城。还是先给大家看一下相关的网站效果: 所以大家如果想快速的学习或者掌握一门编程语言,最好的方案就是通过学习了基础编程知识后,就开始利用…

嵌入式学习——远程终端登录和桌面访问

目录 通过桥接模式连接虚拟机和Windows系统 1、桥接模式 2、虚拟机和Windows连接(1) 3、虚拟机和Windows连接(2) 在Linux虚拟机中创建新用户 Windows系统环境下对Linux系统虚拟机操作 远程登录虚拟机(1&#xff…

如何新建一个空分支(不继承 master 或任何提交)

一、需求分析: 在 Git 中,我们通常通过 git branch 来新建分支,这些分支默认都会继承当前所在分支的提交记录。但有时候我们希望新建一个“完全干净”的分支 —— 没有任何提交,不继承 master 或任何已有内容,这该怎么…

Qt编写推流程序/支持webrtc265/从此不用再转码/打开新世界的大门

一、前言 在推流领域,尤其是监控行业,现在主流设备基本上都是265格式的视频流,想要在网页上直接显示监控流,之前的方案是,要么转成hls,要么魔改支持265格式的flv,要么265转成264,如…

[第十六届蓝桥杯 JavaB 组] 真题 + 经验分享

A:逃离高塔(AC) 这题就是简单的签到题,按照题意枚举即可。需要注意的是不要忘记用long,用int的话会爆。 📖 代码示例: import java.io.*; import java.util.*; public class Main {public static PrintWriter pr ne…

深⼊理解 JVM 执⾏引擎

深⼊理解 JVM 执⾏引擎 其中前端编译是在 JVM 虚拟机之外执⾏,所以与 JVM 虚拟机没有太⼤的关系。任何编程语⾔,只要能够编译出 满⾜ JVM 规范的 Class ⽂件,就可以提交到 JVM 虚拟机执⾏。⾄于编译的过程,如果你不是想要专⻔去研…

iwebsec靶场 文件包含关卡通关笔记11-ssh日志文件包含

目录 日志包含 1.构造恶意ssh登录命令 2.配置ssh日志开启 (1)配置sshd (2)配置rsyslog (3)重启服务 3.写入webshell木马 4.获取php信息渗透 5.蚁剑连接 日志包含 1.构造恶意ssh登录命令 ssh服务…

kafka菜鸟教程

一、kafka原理 1、kafka是一个高性能的消息队列系统,能够处理大规模的数据流,并提供低延迟的数据传输,它能够以每秒数十万条消息的速度进行读写操作。 二、kafka优点 1、服务解耦 (1)提高系统的可维护性‌ 通过服务…

应用镜像是什么?轻量应用服务器的镜像大全

应用镜像是轻量应用服务器专属的,镜像就是轻量应用服务器的装机盘,应用镜像在原有的纯净版操作系统上集成了应用程序,例如WordPress应用镜像、宝塔面板应用镜像、WooCommerce等应用,阿里云服务器网aliyunfuwuqi.com整理什么是轻量…

深入理解分布式缓存 以及Redis 实现缓存更新通知方案

一、分布式缓存简介 1. 什么是分布式缓存 分布式缓存:指将应用系统和缓存组件进行分离的缓存机制,这样多个应用系统就可以共享一套缓存数据了,它的特点是共享缓存服务和可集群部署,为缓存系统提供了高可用的运行环境&#xff0c…

Spring Boot 中的自动配置原理

2025/4/6 向全栈工程师迈进! 一、自动配置 所谓的自动配置原理就是遵循约定大约配置的原则,在boot工程程序启动后,起步依赖中的一些bean对象会自动的注入到IOC容器中。 在讲解Spring Boot 中bean对象的管理的时候,我们注入bean对…

剑指Offer(数据结构与算法面试题精讲)C++版——day16

剑指Offer(数据结构与算法面试题精讲)C版——day16 题目一:序列化和反序列化二叉树题目二:从根节点到叶节点的路径数字之和题目三:向下的路径节点值之和附录:源码gitee仓库 题目一:序列化和反序…

windows server C# IIS部署

1、添加IIS功能 windows server 2012、windows server 2016、windows server 2019 说明:自带的是.net 4.5 不需要安装.net 3.5 尽量使用 windows server 2019、2016高版本,低版本会出现需要打补丁的问题 2、打开IIS 3、打开iis应用池 .net 4.5 4、添…

【教程】PyTorch多机多卡分布式训练的参数说明 | 附通用启动脚本

转载请注明出处:小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你,欢迎[点赞、收藏、关注]哦~ 目录 torchrun 一、什么是 torchrun 二、torchrun 的核心参数讲解 三、torchrun 会自动设置的环境变量 四、torchrun 启动过程举例 机器 A&#…

Neo4j初解

Neo4j 是目前应用非常广泛的一款高性能的 NoSQL 图数据库,其设计和实现专门用于存储、查询和遍历由节点(实体)、关系(边)以及属性(键值对)构成的图形数据模型。它的核心优势在于能够以一种自然且…