一、协程是什么?
协程是一个代码块,它跟一个方法一样,有着开始和结束的地方,开始和结束之间就是这个协程的作用域。
在协程的作用域内,它可以在任意一个点挂起(暂停下来,并释放开启协程的线程),把一项工作丢给其他线程处理,等这项工作执行完毕带着结果返回,它又能在挂起点(回到开起协程的线程)继续执行。
用代码表示如下:
// 1. 这是一个普通的、运行在主线程(UI线程)的方法
fun initData() {
// 2. 从UI线程开启协程,进入协程作用域
lifecycleScope.launch(Dispatchers.Main) {
// 🏁 【阶段 A】此时在主线程,可以做展示 UI 的工作
println("当前线程:${Thread.currentThread().name}") // 打印:main
// 当前协程在这里【挂起】,这个挂起包含以下操作:
// 1 保存当前状态,比如变量值
// 2 把withContext后续代码包装成一个带有状态,可执行任务
// 3 把withContext花括号的任务丢给 IO线程,
// 4 暂停执行ui线程,return,主线程立刻被释放,去做刷新UI等其他事情)
val result = withContext(Dispatchers.IO) {
// 🏁 【阶段 B】此时进入了后台IO线程
println("当前线程:${Thread.currentThread().name}") // 打印:DefaultDispatcher-worker-1
"这是从后台拿到的数据" // 这行是返回值
}
// 🏁 【阶段 C】时空恢复!withContext作用域结束,自动切回协程发起线程
// 拿到了 result 变量,代码在暂停点无缝继续向下执行,
println("当前线程:${Thread.currentThread().name}") // 打印:main
println("拿到结果:$result")
}
// 协程作用域结束
}
二、协程原理是什么
【 阶段一:协程发起 】
[主线程] 调用 lifecycleScope.launch(Dispatchers.Main) { ... }
│
▼ 编译器:将花括号内的全部代码,打包 new 成一个Runnable
▼(这个 Runnable 在协程界叫 Continuation
│
▼ 默认触发第一次派发:通过 Handler.post(Task_Continuation) 丢进主线程 MessageQueue
│
[主线程 Looper] 轮询到该任务 -> 执行 Task_Continuation.run() -> 协程正式在主线程开跑!
│
├─ 🏁 执行 launch 内部代码(如展示 Loading 动画)
│
▼ 遇到 withContext(Dispatchers.IO) { api.getData() }
──────────────────────────────────────────────────────────────────────────────
【 阶段二:切换 】与【 阶段三:脱身 】
│
├─ 1. 保存变量状态+把withContext后续代码包装成一个可执行任务,叫Task_Continuation
│
├─ 2. 将withContext 花括号内的任务(api.getData())封装成一个标准的普通 Runnable,
│ 并将其塞进 [IO 线程池队列] 异步执行。
│
└─ 3. withContext 在主线程瞬间返回一个特殊的底层令牌 —— COROUTINE_SUSPENDED。
│
▼ launch 收到该令牌,主线程直接通过 return 强行跳出当前整个 launch 块!
▼
[主线程解脱] ────> 瞬间回到系统 Looper 循环,欢快地去刷新 UI、响应用户滑动(界面不卡顿)
──────────────────────────────────────────────────────────────────────────────
【 阶段四:劳作 】
[IO 线程池某个后台子线程]
│
├─ 4. 接单:在后台啪嗒啪嗒跑完 api.getData() 耗时任务。
└─ 5. 完工:拿到了网络数据结果 `result`。
──────────────────────────────────────────────────────────────────────────────
【 阶段五:回归 】
│
▼ 回归:后台线程掏出刚才主线程传过来的 [Task_Continuation ]
▼ 调用 DispatchedContinuation.resume(result)
│
[主线程 Handler] ◄──────────────────────┘
│
▼ 底层物理执行:mainHandler.post(Task_Continuation)
▼(把这个带有状态的超级 Runnable 重新扔回主线程的排队队列中)
│
[主线程 Looper]
│
▼ 轮询排队到它 ─> 主线程再次执行 Task_Continuation.run()
│
▼ 💥 复活:跳转到 withContext 的下一行代码
│
└─ 🏁 带着后台带回来的 result,直接在主线程刷新 UI 界面!
注意:⚠️⚠️
若开启协程是:lifecycleScope.launch(Dispatchers.Main.immediate) { ... }
│
▼ immediate 机制触发!直接在UI线程继续执行
不会打包runable去排队
从上面可以看出,协程主要作用解决不同子程序块的在不通线程调度执行问题,它会封装异步操作、回调、订阅等,使我们的程序在不同线程上调度执行,而代码则如同顺序执行一样。
如果这种事是我们自己来做呢?其实我们也很容易想到设定一个全局的变量,然后不同的协作任务都能改变这个变量的状态,然后根据变量的状态值,来决定调度哪一个子程序,这叫Switch状态机,协程也是这样做的,总结下就是:
协程调度=状态机+回调
三、什么时候用协程
场景一 多个串行请求
例子场景:先登录 → 拿 token → 请求用户信息 → 请求订单
// 现代协程写法:用显式 withContext 展平四步串行依赖
fun startUserFlow() {
// 1. 诞生:使用 immediate,在主线程原地闪现开跑!
lifecycleScope.launch(Dispatchers.Main.immediate) {
try {
showLoading() // 🏁 主线程:高调亮起 Loading 动画
// ⚡️ 物理暂停点 1:主线程在此脱身!任务被一枪打进 IO 线程池
val authCode = withContext(Dispatchers.IO) {
// ─── 此时时空切换到:后台 IO 线程 ───
authSDK.login(username, password) // 跑耗时网络请求
} // ─── 完工:自动切回主线程 ───
// ⚡️ 物理暂停点 2:主线程再次脱身!带着热腾腾的 authCode 传给第二步
val token = withContext(Dispatchers.IO) {
// ─── 此时时空切换到:后台 IO 线程 ───
apiService.fetchToken(authCode)
} // ─── 完工:自动切回主线程 ───
// ⚡️ 物理暂停点 3:主线程第三次脱身!带着 token 去换用户信息
val userProfile = withContext(Dispatchers.IO) {
// ─── 此时时空切换到:后台 IO 线程 ───
apiService.fetchUserProfile(token)
} // ─── 完工:自动切回主线程 ───
// ⚡️ 物理暂停点 4:主线程最后一次脱身!带着用户 ID 去换订单
val orders = withContext(Dispatchers.IO) {
// ─── 此时时空切换到:后台 IO 线程 ───
apiService.fetchOrders(userProfile.userId)
} // ─── 完工:自动切回主线程 ───
// 🏁 终点:四次跨线程大迁徙完美结束!主线程拿着最终的 orders 刷新 UI
renderOrderList(orders)
} catch (e: Exception) {
// 🛡️ 统驭一切的异常安全网
// 无论是上面 4 个 withContext 块里的哪一个在后台抛出网络断开或 500 错误
// 都会顺着状态机精准轰塌到这里,当场在主线程弹窗提示!
showErrorToast(e.message)
} finally {
// 💯 铁律:无论成功,还是在第 2 步崩溃,都一定会稳稳走到这里关闭 Loading
dismissLoading()
}
}
}
场景二、多路数据并发聚合
例子:地图App 首页打开,界面需要同时展示:
【A:高清地图图层(约500ms)】、
【B:实时路况动态(约1000ms)】、
【C:用户收藏点(约300ms)】。
产品经理要求:三个接口同时并发请求,谁也别等谁,但必须等它们全部拿齐后,界面才能一次性完整刷新,如果任何一个报错,立刻走兜底逻辑。
lifecycleScope.launch {
try {
// 三支箭同时射出,完全并发
val mapLayerDeferred = async(Dispatchers.IO) { sdk.getMapLayer() }
val trafficDeferred = async(Dispatchers.IO) { api.getTraffic() }
val userPrefDeferred = async(Dispatchers.IO) { db.getUserCollect() }
// ⭐️ 完美归流:死等三个结果全部凑齐,一行代码展平
renderUI(mapLayerDeferred.await(), trafficDeferred.await(), userPrefDeferred.await())
} catch (e: Exception) {
// 只要任何一个崩了,自动触发连锁反应,其他未完成的异步任务当场被框架无情掐死,绝不浪费资源
showErrorDefaultUI()
}
}
场景三、高频高压数据流
在车载定位中,底层的硬件传感器正以 每秒 100 次(100Hz) 的恐怖频率源源不断地向应用层吐出原始二进制数据包,应用层需要把这些数据包扔到后台进行矩阵运算,算完后把最新的位置画在屏幕上
因为要画地图、渲染复杂的 3D 轨迹,渲染线程每秒只能处理约 30 个数据,而且在不同车机上处理的数据个数还不同。
上游拼命发,下游处理不完,数据只能堆积在 队列里排队。由于队列越来越长,你在第 10 秒看到的车位轨迹,其实是系统在第 2 秒时收集到的旧数据!导航箭头直接发生严重的时序滞后和拖影。如果内存爆了,车机当场死机。
假设
// 😭 厂商给的原始 SDK:只懂 Listener 监听
public class SensorHardwareManager {
// 厂商的注册监听接口
public interface OnSensorChangedListener {
void onDataArrived(SensorData data);
}
// 厂商提供的原始方法:传入监听器,开始吐数据
public void registerListener(OnSensorChangedListener listener) { ... }
// 厂商提供的原始方法:注销监听,停止吐数据
public void unregisterListener(OnSensorChangedListener listener) { ... }
}
传统做法
public class SensorManager {
// 1. 核心大坝:独占一个容量为 1 的原子引用,专门用来存放最新的一滴水(最新数据)
private final AtomicReference<SensorData> latestData = new AtomicReference<>();
private SensorHardwareManager.OnSensorChangedListener listener;
private boolean isRunning = false;
public void start() {
if (isRunning) return;
isRunning = true;
// 2. 物理现实:硬件厂商的原始 Listener
listener = new SensorHardwareManager.OnSensorChangedListener() {
@Override
public void onDataArrived(SensorData data) {
// 💥 核心魔术:这里跑在硬件厂商的物理子线程上(比如 100Hz 高频触发)
// 来了新水,不管大坝槽里有没有老水,直接无情强制覆盖!
// 这才是纯正、地道的普通 Java 版 conflate() 物理行为!
latestData.set(data);
}
};
// 3. 通电:向硬件注册监听,硬件线程开始疯狂往 latestData 里 set 数据
hardware.registerListener(listener);
// 4. 消费端:主线程(Main)开始像抽水机一样,定时去大坝里抽水
mainHandler.post(new Runnable() {
@Override
public void run() {
if (!isRunning) return;
// 5. 喜新厌旧:主线程每次抬头,只捞走当前最新被顶替进去的那一个
SensorData data = latestData.get();
if (data != null) {
// 安全地在主线程更新车机地图轨迹
updateCarVectorOnMap(data);
}
// 6. 驱动下一帧:每 16ms (60Hz) 往主线程消息队列扔一个定时任务,闭环空转
mainHandler.postDelayed(this, 16);
}
});
}
public void stop() {
isRunning = false;
// 7. 必须手动解绑!如果忘了这行,硬件线程会一直拿着 listener 疯狂 set,直接内存泄漏暴毙!
hardware.unregisterListener(listener);
mainHandler.removeCallbacksAndMessages(null);
}
}
问题:主线程是用 postDelayed(16) 去轮询,如果上游传感器突然不发数据了,主线程依然在白白地每 16 毫秒去 get() 一次空数据
协程做法
// 🛠️ 给硬件类一个扩展属性/函数:.asFlow()
fun SensorHardwareManager.asFlow(): Flow<SensorData> {
// 💥 协程官方必杀技:专门将“回调接口”魔改成“Flow流”的制造工厂
return callbackFlow {
// 1. 创建一个硬件厂商需要的普通 Listener 实例
val listener = SensorHardwareManager.OnSensorChangedListener { data ->
// 2. ⭐️ 核心接力:硬件吐出数据了!
// 我们利用 trySend() 极其高效地把数据一枪打进当前 Flow 的内部管道里!
// 这一步,完成了数据从“旧回调”到“新协程”的物理跨越!
trySend(data)
}
// 3. 通电:调用厂商的原始方法,把这个监听器注册进去,硬件开始啪嗒啪嗒吐数据
registerListener(listener)
// 4. 🛡️ 终极安全锁:当外层的 lifecycleScope 断电(取消)时,
// 整个 Flow 管道关闭,底层会自动触发 awaitClose 块!
awaitClose {
// 💥 功成身退:在这里解绑监听器!防止硬件在后台继续吐数据导致内存泄漏!
unregisterListener(listener)
}
}
}
lifecycleScope.launch(Dispatchers.Main.immediate) {
// 完整的 Flow 水管流水线正式开始组装并运转
sensorDataSource.asFlow() // A. 拧开水龙头(开启高频采集)
.flowOn(Dispatchers.IO) // B. 后台线程池去死循环捞数据
.conflate() // C. 智能阀门:只留最新的,冲掉老数据
.collect { rawData -> // D. 接水杯:数据流的终点,回到主线程
// 🏁 4. 最终消费:在主线程安全、零延迟地绘制轨迹
updateCarVectorOnMap(rawData)
} // 💥 注意:collect 是一个挂起函数,如果没有上面的 launch,这里根本通不过编译!
}
- callbackFlow负责把 Java 的 registerListener框起来;
- .conflate()*负责把 Java 的 AtomicReference缓存槽框起来;
- .collect负责把 Java 的 mainHandler**** 刷新机制框起来。
协程只是把我们过去写得七零八落、长达几十行的传统回调+缓存槽代码,用数据流的水管模型给标准化。”
四 协程使用注意事项
一、suspend没有任何“切线程/异步”的超能力,它是一个警示,这个函数内部有可能会发生挂起(使用withContext挂起),你要一直向上追溯,找到真正调用它的协程作用域才知道它会在哪个点挂起,这对定位一些时序问题有作用。
二、凡是依赖协程内部异步返回结果的代码,必须死死写在 launch 块的内部、挂起函数的下方,因为协程虽然看起来是同步代码,但是实际是最后执行的时候,ui线程已经不知道运行到什么地方了,环境完全变了。**
三、协程的“不卡主线程”,前提是内部的耗时操作必须是协作式挂起的(比如 withContext(Dispatchers.IO)或者内部自带挂起)。如果你要调用传统的阻塞 I/O(读文件、硬核矩阵运算、大循环),必须人肉用 withContext(Dispatchers.IO)将其强行放逐到后台线程池,否则主线程该卡死还是卡死。