协程这样理解容易懂

一、协程是什么?

协程是一个代码块,它跟一个方法一样,有着开始和结束的地方,开始和结束之间就是这个协程的作用域。

在协程的作用域内,它可以在任意一个点挂起(暂停下来,并释放开启协程的线程),把一项工作丢给其他线程处理,等这项工作执行完毕带着结果返回,它又能在挂起点(回到开起协程的线程)继续执行。

用代码表示如下:

// 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)将其强行放逐到后台线程池,否则主线程该卡死还是卡死。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容