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 创建。

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

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

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

4. 协程构建器

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

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

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

​特性

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

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

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

特性​​:

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

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

kotlin 复制代码
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:在阻塞代码中启动协程​

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

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

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

kotlin 复制代码
@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()
挂起协程,释放线程资源 阻塞线程,线程无法执行其他任务
只能在协程或挂起函数中调用 可在任何线程中调用
kotlin 复制代码
viewModelScope.launch {
    repeat(10) {  
        delay(1000)       // 每隔 1 秒执行一次,不阻塞主线程  
        updateCounter(it)  
    }  
}  

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

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

kotlin 复制代码
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 示例​​:并行任务与结果合并
kotlin 复制代码
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() 的异常:

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

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

5.3.3 协程是否存活

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

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

6. 协程的异常处理机制

6.1 异常传播机制

默认规则​​:

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

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

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

6.2 捕获异常的方式

6.2.1 方式 1:try-catch 块

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

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

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

定义异常处理器

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

附加到协程上下文

kotlin 复制代码
viewModelScope.launch(exceptionHandler) {  
    launch { throw IOException() } // 异常会被 exceptionHandler 捕获
}

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

6.3 隔离异常:SupervisorJob

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

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

6.4 自定义异常处理策略

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

​​- 重试机制 ​​:在捕获异常后自动重试任务。

​​- 回退操作​​:失败时返回默认值或缓存数据。

6.4.1 示例:网络请求重试​
kotlin 复制代码
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() 快速失败
kotlin 复制代码
public fun Job.ensureActive(): Unit {
    if (!isActive) throw getCancellationException()
}
6.5.3 ​​谨慎使用 SupervisorJob

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

相关推荐
虽千万人 吾往矣12 分钟前
golang context源码
android·开发语言·golang
天堂的恶魔94622 分钟前
C++项目 —— 基于多设计模式下的同步&异步日志系统(4)(双缓冲区异步任务处理器(AsyncLooper)设计)
开发语言·c++·设计模式
未来之窗软件服务36 分钟前
数字人,磁盘不够No space left on device,修改python 执行环境-云GPU算力—未来之窗超算中心
linux·开发语言·python·数字人
爱的叹息44 分钟前
【java实现+4种变体完整例子】排序算法中【桶排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格
java·开发语言·排序算法
Zhuai-行淮1 小时前
施磊老师基于muduo网络库的集群聊天服务器(二)
开发语言·网络·c++
wangz761 小时前
Gradle 中添加生成 jar 报错
kotlin·gradle·jar
李新_1 小时前
Android 多进程并发控制如何实现
android·java
python_chai2 小时前
Python多进程并发编程:深入理解Lock与Semaphore的实战应用与避坑指南
开发语言·python·高并发·多进程··信号量
ll_god2 小时前
Android 应用wifi direct连接通信实现
android