文章目录
- 一、协程核心基础
- 二、协程四大核心组件
- 三、协程启动方式与场景区别
- 四、协程异常处理体系
- 五、协程核心进阶原理
- 六、Android 项目实战场景
- 七、协程性能优化与避坑
- 八、高频面试真题+精细化标准答案
- 九、补充:协程核心拓展知识
- 十、实战案例
一、协程核心基础
1.1 什么是协程?与线程的核心区别
定义:协程是用户态轻量级线程,基于线程封装,由Kotlin语言层、库层调度,而非操作系统内核调度,是Kotlin官方标准化的异步、并发解决方案,主打低开销、高并发、代码同步式书写。
核心区别(面试高频·精细化对比)
- 资源开销:线程由OS内核创建、调度、销毁,开销大,系统最大并发数仅数千;协程为用户态逻辑任务,无内核态切换开销,单机支持十万级并发,资源占用可忽略
- 调度机制:线程是内核抢占式调度,系统强制切换线程执行权;协程是用户态协作式调度,主动挂起、主动恢复,无强制抢占
- 阻塞特性:线程调用sleep、IO阻塞时,会永久占用当前内核线程,线程空闲率极低;协程suspend挂起时完全释放线程,线程可复用执行其他任务,线程利用率拉满
- 依赖关系:协程无法脱离线程独立运行,始终依托线程池执行,是对线程的逻辑层封装
- 编程体验:线程需手动处理回调、锁、线程切换,易产生回调地狱;协程同步顺序书写异步代码,可读性、可维护性极高
1.2 挂起与恢复
挂起函数 suspend:被 suspend 修饰的函数,仅能在协程作用域或其他挂起函数中调用,普通线程、普通函数无法直接调用。
核心特性:
- 挂起不阻塞线程:网络请求、文件IO、delay延时等等待场景,仅挂起当前协程任务,不占用绑定线程
- 精准可恢复性:等待条件结束后,协程会在原调度器指定线程恢复执行,保留挂起前的局部变量、执行位置
- 异常统一管控:挂起函数不主动抛出受检异常,所有异常统一交由协程上下文异常体系处理
底层编译原理:Kotlin编译器将所有挂起函数编译为有限状态机(FSM),自动拆分代码段、记录执行状态,实现无栈式断点续执行,这是协程挂起恢复的核心底层支撑。
挂起的本质:suspend 经编译器改写为“状态机 + Continuation”。遇到挂起点时保存现场并返回COROUTINE_SUSPENDED。
恢复的本质:挂起条件满足后(delay 到期/IO 回调返回),并不是“立刻抢回原线程A”,而是 resume 后交给Dispatcher.dispatch(…),把 continuation 包装成任务投递/入队。
二、协程四大核心组件
四大核心:CoroutineScope 作用域、Dispatcher 调度器、Job 任务、CoroutineContext 上下文,四者组合构成完整协程运行环境
2.1 CoroutineContext 协程上下文
核心定义:协程的全局配置容器,本质是一个只读键值对集合,支持元素叠加、覆盖、合并,所有协程的运行配置、生命周期、异常策略都存储在此。
核心四要素(全覆盖):
- Dispatcher:线程调度器,决定协程执行的线程环境
- Job:任务控制器,管控协程生命周期、取消、状态监听
- CoroutineName:协程自定义名称,用于日志排查、线上问题定位
- CoroutineExceptionHandler:协程全局异常兜底处理器
上下文合并规则:新上下文元素会覆盖旧上下文同Key元素,不同Key元素相互叠加,这是withContext、launch传参修改配置的底层原理。
2.2 Dispatcher 调度器
决定协程执行线程,是协程线程切换的核心,四大官方调度器工程级精细区别:
- Dispatchers.Main:Android专属主线程调度器,依托Android主线程Handler实现,仅用于UI刷新、页面交互、控件操作,禁止执行耗时任务
- Dispatchers.Default:默认调度器,核心适配CPU密集型任务,核心线程数 = CPU核心数,线程池固定,适合JSON解析、数据排序、算法计算、图片裁剪
- Dispatchers.IO:IO专属调度器,适配阻塞式IO任务,底层与Default共享线程池,支持动态扩容线程,适合网络请求、文件读写、数据库CRUD、SP存储
- Dispatchers.Unconfined:无约束调度器,跟随当前调用线程执行,挂起后恢复线程不确定,项目绝对禁止使用,仅用于测试场景
Default 和 IO 核心区别:两者底层共用同一个核心线程池,Default线程数固定,防止CPU打满卡顿;IO针对阻塞任务做了线程扩容优化,IO阻塞时自动新建线程承接新任务,不会阻塞核心计算线程。
2.3 Job 协程任务(生命周期管控·精细状态+实战坑点)
每一个协程launch/async都会生成唯一Job实例,是协程生命周期的唯一管控对象。
四大精细状态:New(新建未启动)、Active(活跃执行)、Completed(正常完成)、Cancelled(异常/手动取消)
核心特性:
- 默认Job具备父子强绑定机制:父协程取消,所有子协程级联取消;子协程异常崩溃,父协程及所有兄弟协程全部取消(雪崩效应)
- 支持手动管控:job.cancel()取消任务、job.join()阻塞等待任务完成、状态监听回调
SupervisorJob 精细化区别:
SupervisorJob:完全隔离子任务异常,单个子协程崩溃,不影响父协程、不影响其他子协程,完美适配多独立接口并行、批量上报场景。
实战用法:页面多接口并行请求时,自定义Scope必须搭配SupervisorJob,避免单个接口失败导致所有请求终止。
2.4 CoroutineScope 协程作用域(工程规范+实战场景)
协程的运行容器,所有协程必须依托作用域启动,核心价值:绑定业务生命周期,统一批量取消任务,彻底杜绝协程内存泄漏。
Android 四大常用作用域·精细适用场景:
- GlobalScope:全局进程级作用域,生命周期跟随APP进程,业务代码禁止使用。仅用于全局日志、进程级常驻任务,页面业务使用必泄漏
- viewModelScope:ViewModel专属作用域,默认绑定SupervisorJob+Main调度器,ViewModel销毁自动cancel,无内存泄漏,是页面业务首选作用域
- lifecycleScope:Activity/Fragment专属,绑定页面生命周期,页面onDestroy自动取消所有协程,适合页面临时异步任务
- 自定义Scope:独立业务组件、工具类专用,手动绑定组件生命周期,销毁主动cancel
三、协程启动方式与场景区别
3.1 launch 与 async/await 核心区别+落地场景 **
- launch
无返回值异步任务启动器,返回Job对象,仅用于执行无需结果的异步任务,侧重「执行过程」。
实战场景:埋点上报、日志打印、延时弹窗、无需回调的后台任务
- async/await
有返回值异步任务启动器,返回Deferred泛型对象,通过await()阻塞获取任务结果,侧重「执行结果」,支持并行计算。
实战场景:多接口并行请求、多文件并行读取、多任务并发计算
launch:开启协程,返回 Job,无业务返回值,适合不需要拿到结果的任务;内部异常直接抛出。
async:开启协程,返回 Deferred,用于产生返回值;任务立刻后台执行,本身不会等待。
await:挂起函数,不启动协程,阻塞等待 Deferred 拿到结果;异常在 await 调用点抛await:暂停当前协程,等待目标 async 任务做完,拿到返回值,再继续往下跑。
异常关键点:
launch:内部异常直接抛出,父协程直接崩溃取消。async:异常会被存到 Deferred,只有调用 await () 才抛出异常,不调用 await,异常不会触发
3.2 串行执行 vs 并行执行
- 串行执行(耗时叠加)
顺序调用await,上一个任务完成再执行下一个,总耗时 = 所有任务耗时总和,适合有依赖的任务(先查缓存、再请求网络)。
//串行,顺序await
val a = apiA()
val b = apiB()
- 并行执行(耗时取最大)
先批量启动async任务,再统一await,任务并发执行,总耗时 = 耗时最长的任务,适合无依赖的独立任务,大幅优化页面加载速度。
//并行:先全部启动,再等待结果
val deferredA = async { apiA() }
val deferredB = async { apiB() }
val resultA = deferredA.await()
val resultB = deferredB.await()
工程硬性规范:首页多个无依赖接口,必须使用并行async,禁止串行,减少页面白屏、加载耗时。
3.2.1 实现串行的几种方式
- 方式 1:直接顺序调用(最基础,自动串行)
import kotlinx.coroutines.*
fun main() = runBlocking {
// 协程内部顺序执行 → 串行
launch {
task1()
task2()
task3()
}.join()
}
suspend fun task1() {
delay(1000)
println("任务1完成")
}
suspend fun task2() {
delay(1000)
println("任务2完成")
}
suspend fun task3() {
delay(1000)
println("任务3完成")
}
- 方式 2:async 顺序 await
runBlocking {
val r1 = async { task1() }.await()
val r2 = async { task2() }.await()
val r3 = async { task3() }.await()
}
- 方式 3:循环串行执行一批任务
runBlocking {
val tasks = listOf(::task1, ::task2, ::task3)
for (t in tasks) t()
}
✅ 串行特点:后一个依赖前一个结果时,适合串行。
3.2.2 实现并行
- 方式 1:async + awaitAll(推荐)
fun main() = runBlocking {
val deferreds = listOf(
async { task1() },
async { task2() },
async { task3() }
)
// 等待全部并行任务完成
deferreds.awaitAll()
println("全部并行任务结束")
}
- 方式 2:launch + joinAll(不需要返回值)
runBlocking {
val jobs = listOf(
launch { task1() },
launch { task2() },
launch { task3() }
)
jobs.joinAll()
}
- 方式 3:批量集合并行(动态任务列表)
runBlocking {
val tasks = listOf(::task1, ::task2, ::task3)
val deferreds = tasks.map { async { it() } }
deferreds.awaitAll()
}
四、协程异常处理体系
4.1 两种异常传播机制(核心本质)
- launch 异常:异常主动向上传播,触发协程崩溃,父、子所有关联协程全部取消,直接抛出崩溃信息
- async 异常:异常延迟封装,存储在Deferred对象中,不主动传播,仅在调用await()时才会抛出,不调用则无任何崩溃提示(隐藏坑点)
4.2 三种异常捕获方案(优先级+适用场景)
- 局部try-catch(首选):精准包裹挂起函数/await方法,单独捕获单个任务异常,不影响其他任务,精度最高、工程最常用
- CoroutineExceptionHandler(全局兜底):作用域级统一异常捕获,仅能捕获launch主动传播的异常,无法捕获async await异常,用于全局崩溃兜底上报
- SupervisorJob(异常隔离):阻断异常雪崩,实现单任务失败、整体任务正常执行,适配批量并行任务
4.3 高频面试坑点精解
问题:为什么全局异常处理器抓不到 async 异常?
标准答案:async属于延迟异常机制,异常不会主动向上传播,仅在await()调用时被动抛出,全局异常处理器仅监听主动传播的协程异常,因此无法捕获,必须手动try-catch await方法。
4.4 异常处理标准实战模板
// 并行任务+异常隔离+精准捕获,工程最优范式
viewModelScope.launch(SupervisorJob()) {
val task1 = async(Dispatchers.IO) { api1() }
val task2 = async(Dispatchers.IO) { api2() }
// 单个任务异常不影响其他任务
try {
task1.await()
} catch (e: Exception) {
// 单独处理接口1异常
}
try {
task2.await()
} catch (e: Exception) {
// 单独处理接口2异常
}
}
五、协程核心进阶原理
5.1 挂起函数无线程阻塞核心原理
传统线程Thread.sleep/IO阻塞:线程进入阻塞状态,永久占用当前内核线程,线程无法处理其他任务,资源利用率极低。
协程suspend挂起:触发挂起后,立即释放当前绑定的线程,线程回归线程池复用,协程任务进入等待队列;当IO/延时任务完成,调度器重新分配线程恢复任务执行,这是协程高并发、高效率的根本原因。
5.2 协程线程切换原理(withContext精细解析)
withContext(Dispatchers.XXX) 是协程唯一正规线程切换方式,原理:触发协程挂起 → 脱离原有线程 → 在目标调度器线程恢复执行后续代码,基于状态机保存执行上下文,无需手动Handler回调、线程切换。
实战优势:子线程IO请求、主线程刷新UI,代码自上而下顺序书写,彻底解决回调地狱,代码可读性、可维护性大幅提升。
// 标准线程切换模板
viewModelScope.launch {
// 1. 子线程执行IO网络请求
val data = withContext(Dispatchers.IO) {
apiService.getHomeData()
}
// 2. 自动切回主线程更新UI
updateUI(data)
}
5.3 父子协程层级与取消机制
默认Job强绑定:父协程取消,所有子协程立即终止;子协程异常,父协程直接取消,产生任务雪崩。
SupervisorJob解绑层级:仅阻断子异常向上传播,父协程取消依然会终止所有子协程(核心坑点),仅实现子异常不影响全局,并非完全解绑生命周期。
六、Android 项目实战场景
6.1 网络请求工业级标准写法
核心规范:viewModelScope承载 + IO调度请求 + Main刷新UI + try-catch兜底 + 自动取消防泄漏
fun loadHomeData() {
viewModelScope.launch(Dispatchers.Main) {
try {
// 子线程IO请求
val result = withContext(Dispatchers.IO) {
api.loadHomeInfo()
}
// 主线程更新UI
refreshView(result)
} catch (e: Exception) {
// 统一异常处理:报错、兜底UI
showErrorToast()
}
}
}
6.2 延时任务/轮询任务替代Handler
使用delay()替代Handler.postDelayed,无需手动移除任务,页面销毁自动取消,无内存泄漏。
// 3秒延时弹窗
viewModelScope.launch {
delay(3000)
showDialog()
}
// 轮询请求(自动终止)
viewModelScope.launch {
while (isActive) { // 协程活跃状态判断,销毁自动终止循环
pollNetData()
delay(2000)
}
}
6.3 多接口并行容错请求
SupervisorJob异常隔离 + async并行 + 单接口容错,单个接口失败不影响页面整体渲染
fun loadMultiData() {
viewModelScope.launch(SupervisorJob()) {
val listDeferred = async(Dispatchers.IO) { api.getList() }
val userDeferred = async(Dispatchers.IO) { api.getUser() }
// 容错处理
val list = try { listDeferred.await() } catch (e: Exception) { emptyList() }
val user = try { userDeferred.await() } catch (e: Exception) { null }
// 刷新UI
refreshData(list, user)
}
}
6.4 协程防抖实战(解决快速点击重复请求)
通过Job状态判断任务是否活跃,杜绝短时间重复点击触发多次请求
private var requestJob: Job? = null
fun submitData() {
// 任务活跃则直接返回,防抖拦截
if (requestJob?.isActive == true) return
requestJob = viewModelScope.launch(Dispatchers.IO) {
// 提交网络请求
api.submit()
}
}
七、协程性能优化与避坑
7.1 内存泄漏终极规避规范
- 禁止GlobalScope处理页面业务,仅用于进程级常驻任务
- 页面异步任务统一使用lifecycleScope、viewModelScope,依托生命周期自动取消
- 自定义业务Scope必须在组件销毁、页面退出时手动cancel
- 协程内部禁止直接持有Activity/View强引用,避免页面销毁后任务残留持有引用
7.2 高频性能坑点精细化解析
- 主线程协程做耗时计算:Dispatchers.Main协程内执行JSON解析、排序等计算任务,阻塞主线程,引发UI卡顿、ANR,必须切Default/IO调度器
- 无依赖接口串行请求:浪费网络耗时,必须改为async并行执行,优化加载速度
- 未处理async延迟异常:线上隐藏崩溃隐患,偶现闪退,必须全部try-catch包裹await
- 无限循环协程无状态判断:while死循环未判断isActive,页面销毁后依然后台死循环,造成性能损耗、泄漏
八、高频面试真题+精细化标准答案
1. 协程为什么比线程更高效?
协程是用户态轻量级逻辑任务,创建销毁无内核态切换开销,支持十万级并发;核心优势是挂起不阻塞线程,最大化复用线程池资源,线程利用率远高于传统线程。而线程为内核调度,阻塞会永久占用线程资源,并发能力弱、开销大。
2. viewModelScope 为什么不会内存泄漏?
viewModelScope绑定ViewModel生命周期,自带SupervisorJob,当页面销毁、ViewModel被系统回收时,作用域会自动调用cancel()终止所有协程任务,清空所有引用,杜绝页面View、Activity引用残留导致的内存泄漏。
3. SupervisorJob 和普通 Job 核心区别?
普通Job父子协程强绑定,子协程异常会触发全局任务雪崩,父子、兄弟协程全部取消;SupervisorJob实现子任务异常隔离,单个子协程失败仅终止自身,不影响父协程和其他子协程,适合多独立并行任务场景。
4. launch 和 async 适用场景区别?
launch用于无返回值、无需结果的异步任务,如埋点、日志、延时操作;async用于需要获取返回结果的异步任务,支持多任务并行,适用于多接口同时请求、批量数据读取等需要结果回调的场景。
5. 协程线程切换底层原理?
通过withContext切换调度器,触发协程挂起,依托编译器生成的状态机保存当前执行上下文,释放原有线程,在目标调度器线程恢复执行后续代码,实现无回调、顺序式线程切换。
6. 全局异常处理器为什么捕获不到 async 异常?***
async采用延迟异常机制,异常封装在Deferred对象中,不会主动向上传播,仅在调用await()时被动抛出;而CoroutineExceptionHandler仅捕获主动传播的协程异常,因此无法捕获,必须手动try-catch处理。
7. Dispatchers.IO 和 Default 区别?***
两者底层共享核心线程池;Default适配CPU密集型计算,线程数固定为CPU核心数,避免CPU过载;IO适配阻塞式IO任务,支持线程池动态扩容,解决IO阻塞导致的线程占用问题,吞吐量更高。
九、补充:协程核心拓展知识
9.1 isActive 状态判断核心用法 **
协程取消后,内部依然会执行完当前代码块,不会立即终止。在无限循环、长耗时任务中,必须通过 isActive 判断协程活跃状态,及时终止任务,避免无效执行、性能损耗。
实战场景:轮询任务、批量数据遍历、大文件读写
9.2 常用协程 API 详解
协程库提供了丰富的 API 来简化并发编程。以下是一些核心且高频的 API 及其工程化用法。
9.2.1 并发与等待 API
awaitAll(vararg deferreds: Deferred<T>): List<T>
- 作用:并发执行多个
Deferred任务,并等待所有任务完成。所有任务成功则返回结果列表;任意一个任务抛出异常,则整个awaitAll调用会抛出该异常(CancellationException除外)。 - 场景:并行请求多个无依赖的接口,并等待全部结果返回后再统一处理。
- 代码示例:
viewModelScope.launch { val deferred1 = async { api.fetchUserInfo() } val deferred2 = async { api.fetchProductList() } val deferred3 = async { api.fetchBannerData() } try { // 等待三个接口全部完成 val results: List<Any> = awaitAll(deferred1, deferred2, deferred3) val userInfo = results[0] as UserInfo val productList = results[1] as List<Product> val bannerData = results[2] as List<Banner> // 更新UI updateUI(userInfo, productList, bannerData) } catch (e: Exception) { // 任何一个接口失败,都会进入catch showError("部分数据加载失败") } }
completeDeferred(value: T): Boolean
- 完整签名:
fun <T> CompletableDeferred<T>.complete(value: T): Boolean - 作用:手动为一个
CompletableDeferred实例设置成功结果。返回true表示设置成功(该 Deferred 尚未完成),返回false表示该 Deferred 已处于完成或取消状态。 - 核心场景:将回调(Callback)或监听器(Listener)模式转换为协程的挂起函数。通常在回调中调用此方法来“唤醒”等待中的
await()。 - 代码示例:将传统的网络回调封装为挂起函数。
suspend fun fetchDataSuspend(): Data { val deferred = CompletableDeferred<Data>() // 传统回调API oldApi.fetchData(object : Callback<Data> { override fun onSuccess(data: Data) { // 回调成功,手动完成Deferred deferred.complete(data) } override fun onFailure(error: Throwable) { // 回调失败,手动完成异常 deferred.completeExceptionally(error) } }) // 挂起,等待回调触发complete return deferred.await() }
9.2.2 超时控制 API
withTimeout(timeMillis: Long): T
withTimeoutOrNull(timeMillis: Long): T?
- 作用:为一段挂起代码块设置执行超时。
withTimeout超时会抛出TimeoutCancellationException;withTimeoutOrNull超时则安静地返回null。 - 场景:网络请求、数据库查询、文件操作等需要限制最大耗时的场景。
- 代码示例:
viewModelScope.launch { // 方案1:超时抛出异常 try { val result = withTimeout(5000) { // 5秒超时 api.loadHeavyData() } } catch (e: TimeoutCancellationException) { showToast("请求超时") } // 方案2:超时返回null(更简洁) val resultOrNull = withTimeoutOrNull(3000) { api.loadConfig() } if (resultOrNull == null) { // 超时处理 loadLocalCache() } }
9.2.3 结构化并发与作用域 API
coroutineScope { }
supervisorScope { }
- 作用:创建一个新的子作用域。它们都是挂起函数,会等待其内部所有子协程完成后才完成自身。这是实现结构化并发的关键。
coroutineScope:内部任一子协程失败,会取消整个作用域及其所有子协程。supervisorScope:内部子协程失败不会取消其他子协程或父作用域,实现了异常隔离。
- 场景:当你需要在一个挂起函数内部并发启动多个子协程,并希望它们作为一个整体被管理时。
- 代码示例:
suspend fun fetchUserAndPosts(userId: String): Pair<User, List<Post>> = coroutineScope { // 这两个async是兄弟关系,都在coroutineScope管理下 val userDeferred = async { api.getUser(userId) } val postsDeferred = async { api.getPosts(userId) } // 等待两者都完成 val user = userDeferred.await() val posts = postsDeferred.await() Pair(user, posts) } // 在viewModel中调用 viewModelScope.launch { val (user, posts) = fetchUserAndPosts("123") // 使用结果 }
9.2.4 任务控制与状态 API
Job 状态属性
isActive: 协程是否处于活跃状态(已启动且未完成/未取消)。在循环或长任务中必须检查。isCompleted: 协程是否已完成(正常结束或取消)。isCancelled: 协程是否已被取消。- 示例:
viewModelScope.launch { var i = 0 while (isActive) { // 关键检查! // 模拟工作 delay(1000) i++ if (i > 10) break } Log.d("Coroutine", "Job completed. isActive=$isActive, isCancelled=$isCancelled") }
cancelAndJoin()
- 作用:取消一个 Job 并挂起等待其完成(包括所有清理工作)。比单独调用
cancel()后join()更简洁安全。 - 场景:需要确保一个任务被完全取消并清理后再执行后续操作。
private var dataLoadingJob: Job? = null fun loadData() { dataLoadingJob?.cancelAndJoin() // 等待旧任务完全停止 dataLoadingJob = viewModelScope.launch { // 加载新数据 } }
ensureActive()
- 作用:如果当前协程已不活跃(
isActive为 false),则立即抛出CancellationException。是if (!isActive) throw CancellationException()的简洁写法。 - 场景:在可能耗时的循环或计算中,频繁检查协程状态,及时响应取消。
viewModelScope.launch { for (item in largeList) { ensureActive() // 每次循环都检查,及时退出 processItem(item) // 耗时处理 } }
yield()
- 作用:主动挂起当前协程,将线程让给其他协程执行,然后立即恢复。用于避免一个协程长时间独占线程,提升响应公平性。
- 场景:在 CPU 密集型的循环中,防止阻塞事件循环(特别是在单线程调度器如
Dispatchers.Main上)。viewModelScope.launch(Dispatchers.Default) { for (i in 1..1_000_000) { heavyComputation(i) yield() // 每计算一次就让出一次,避免卡死线程 } }
9.2.5 async 的启动模式:CoroutineStart.LAZY
- 作用:
async(start = CoroutineStart.LAZY)创建的Deferred任务不会立即启动,只有在首次调用await()或start()时才会开始执行。 - 场景:需要延迟计算、或按需触发异步任务时。
- 代码示例:
viewModelScope.launch { // 定义任务,但不执行 val lazyTask = async(start = CoroutineStart.LAZY) { println("Lazy task executed!") expensiveComputation() } // ... 其他操作 if (needResult) { val result = lazyTask.await() // 此时才开始执行计算 } // 如果 never call await(), the task never runs. }
9.2.6 runblocking
runBlocking 返回的是协程作用域 CoroutineScope,但它本身是一个普通顶层函数,不是接口 / 类定义的 Scope
补充类似用法对比

9.3 深入理解join的挂起含义
一个很严重的误区: job.join挂起的不是job这个协程,而是调用job的那个协程;
实例 1:简单等待单个协程完成再往下执行
viewModelScope.launch(Dispatchers.Main) {
val job = launch(Dispatchers.IO) {
println("开始网络请求")
delay(1500) //模拟网络
println("网络请求完成")
}
println("我不会等job,直接走到这里")
job.join() //挂起当前Main协程,等待job执行完毕
println("网络结束之后,才执行更新UI")
}
//输出:1和2的顺序不是固定的,可能反过来
1.我不会等job,直接走到这里
2.开始网络请求
3.网络请求完成
4.网络结束之后,才执行更新UI
实际上不推荐这么写,正确的写法是:
viewModelScope.launch(Dispatchers.Main) {
println("准备发起网络")
// 切换IO,这里会挂起外层协程,等待IO块全部执行完,再回来主线程
withContext(Dispatchers.IO) {
println("开始网络请求")
delay(1500)
println("网络请求完成")
}
// 一定等上面全部跑完,才走到这里更新UI
println("网络结束之后,才执行更新UI")
}
delay和join的差异
- delay 是我自己睡一段时间,我把线程交出去;
- join 是我等另一个协程干完活,我自己挂起让出线程,但是那个被等待的协程继续跑,不会停下来。
十、实战案例
题 1:同时发起 3 个独立接口,全部返回后合并渲染,任一失败整体失败
思路:coroutineScope + async 并发;coroutineScope 语义就是 “所有子协程完成才返回,任一异常全部取消”,正好匹配 “整体失败” 需求。
suspend fun loadPage(): PageData = coroutineScope {
val d1 = async(Dispatchers.IO) { api.userInfo() }
val d2 = async(Dispatchers.IO) { api.config() }
val d3 = async(Dispatchers.IO) { api.banner() }
PageData(d1.await(), d2.await(), d3.await())
}
题 2:接口超时 + 失败重试 3 次,带退避
思路:withTimeout 包单次请求;外层循环计数,指数退避 delay。
suspend fun <T> requestWithRetry(
times: Int = 3,
block: suspend () -> T
): T {
repeat(times - 1) { attempt ->
try {
return withTimeout(3000) { block() }
} catch (e: Exception) {
if (e is CancellationException) throw e
delay(300L * (attempt + 1)) // 退避
}
}
return withTimeout(3000) { block() } // 最后一次不捕获
}
题 3:SSE / 流式接口,页面退出时安全关闭连接
思路:持有 Job,退出时 cancelAndJoin(),保证 finally 里的流关闭执行完,避免连接泄漏。
private var sseJob: Job? = null
fun startSse() {
// 启动新连接前,关闭旧连接
sseJob?.cancel()
sseJob = viewModelScope.launch(Dispatchers.IO) {
try {
api.streamChat().collect { chunk -> _ui.emit(chunk) }
} finally {
sseClient.close() // 取消时必走
}
}
}
override fun onCleared() {
super.onCleared()
}
题 4:搜索框防抖 + 最新请求生效,旧请求丢弃
思路:Flow.debounce 防抖 + collectLatest 自动取消上一次未完成的请求。
viewModelScope.launch {
searchQuery
.debounce(300) // 停止输入300ms后触发
.filter { it.length >= 2 }
.collectLatest { kw -> // 新值来,取消上一次collect
val result = withContext(Dispatchers.IO) { api.search(kw) }
_ui.value = result
}
}
题 5:串行执行任务链,中间可取消,异常统一处理
思路:withContext 串行切换线程(不新建协程),外层 try-catch 兜底。
对比题1,为啥async是并行,withContext是串行
withContext不会创建新的协程,只是挂起当前协程(下述案例就是Main);Async是创建一个新的协程,然后丢给调度队列去执行
viewModelScope.launch {
try {
val token = withContext(Dispatchers.IO) { api.refreshToken() }
val data = withContext(Dispatchers.IO) { api.fetchData(token) }
val cached = withContext(Dispatchers.IO) { db.save(data) }
_ui.value = cached
} catch (e: Exception) {
_ui.value = Error(e.message)
}
}
题 6:多个任务并发,但只要第一个成功返回,其余取消
思路:select + async,谁先 onAwait 返回就用谁,其余 cancel。
场景:主备接口竞速、多 CDN 节点测速。
suspend fun fastest(): String = coroutineScope {
val d1 = async { api.fromCDN() }
val d2 = async { api.fromOrigin() }
val result = select<String> {
d1.onAwait { it }
d2.onAwait { it }
}
coroutineContext.cancelChildren() // `select`只是**选择第一个就绪事件,不会自动取消其它 async 任务**。
// `coroutineContext.cancelChildren()`:取消当前父上下文的全部子 Job,
// **父协程自己不取消**,只干掉 d1/d2 剩下那个慢任务。
result
}
select
select是协程的选择表达式:监听多个可挂起的事件,哪一个事件最先完成,就执行对应分支,整个 select 立刻返回这个结果,其它分支不再处理。
select 会挂起当前协程,直到第一个事件就绪,就结束 select。
deferred.onAwait { it }
onAwait是 select 的子从句:监听一个Deferred(async 返回对象),当这个 async 执行完成,就执行 lambda,lambda 返回值作为 select 整体返回值。
- d1.onAwait {it}:d1 完成 → select 取 d1 的结果
题 7:生命周期感知 —— 页面不可见时暂停轮询,可见恢复
思路:lifecycleScope.launchWhenStarted,进入后台自动挂起,回前台恢复,销毁自动取消。
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
while(true){
val data = withContext(Dispatchers.IO){ api.poll() }
updateUi(data)
delay(5000)
}
}
}
1472



被折叠的 条评论
为什么被折叠?



