Kotlin 协程:一文搞懂 CoroutineExceptionHandler 与 ContinuationInterceptor 的区别

41 阅读3分钟

先说结论:它们是两个完全独立的协程上下文元素——一个决定协程"在哪跑",一个决定协程"炸了怎么办"。

ContinuationInterceptorCoroutineExceptionHandler
职责决定协程在哪个线程执行处理未捕获异常
触发时机每次挂起恢复协程因异常终止时
典型实现Dispatchers.IO / Main / Default自定义异常日志、上报
相互关系互不继承、互不覆盖、互不依赖

一、类型体系:是"兄弟",不是"父子"

两者都是 CoroutineContext.Element 的直接子接口,平级、无继承关系:

interface CoroutineExceptionHandler : CoroutineContext.Element {
    fun handleException(context: CoroutineContext, exception: Throwable)
}
​
interface ContinuationInterceptor : CoroutineContext.Element {
    fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
}

关键点:

  • 各自持有独立的 Key,在上下文中是两个独立的键值对
  • 一个对象不可能同时是两者(除非手动实现两个接口)
  • 常用的 CoroutineDispatcher 只是 ContinuationInterceptor 的抽象子类,负责线程调度;异常处理器完全不参与调度逻辑

二、职责分工:调度中枢 vs 崩溃安全网

ContinuationInterceptor:协程的"调度中枢"

  • 协程每次从挂起状态恢复时,interceptContinuation() 都会被调用,对续体(Continuation)进行包装
  • 决定协程接下来在哪个线程执行
  • Dispatchers.Main / IO / Default 的底层实现就是它,是协程异步调度的"门卫"

CoroutineExceptionHandler:协程的"崩溃安全网"

  • 只在协程遇到未捕获异常时才触发
  • 不改变线程执行路径,也不参与挂起恢复流程
  • 唯一作用:协程因异常终止时提供一个统一的回调入口,避免异常直接裸奔导致崩溃

一句话总结:拦截器决定协程"在哪跑",异常处理器决定协程"炸了怎么办"。

三、执行时序:拦截器贯穿全程,异常处理器最后出场

  1. 协程启动,拦截器介入,决定协程在指定调度器上运行
  2. 执行业务逻辑,每次挂起恢复都由拦截器分发线程
  3. 抛出未捕获异常 → 状态机终止,异常沿父协程链向上传播
  4. 框架从当前上下文查找 CoroutineExceptionHandler,回调 handleException()
  5. Handler 执行日志打印、埋点上报等兜底逻辑

所以别指望异常处理器去改变协程的执行线程——它出场时,协程已经快结束了。

四、上下文组合:不同的 Key,互不覆盖

val context = myExceptionHandler + Dispatchers.IO + Job() + CoroutineName("MyTask")
  • Handler 和 Dispatcher 各占一个 Key,共存于同一上下文,与 + 号位置无关
  • 只有同类型元素才会互相覆盖:两个 Handler 或两个 Dispatcher,右侧覆盖左侧

五、三个最常见的坑

坑 1:以为 Handler 能兜住所有异常

它只捕获"未被捕获且向上传播到父协程"的异常。子协程内部被 try-catch 掉的异常,Handler 根本不会触发。

坑 2:把 Handler 设置在子协程上

普通父协程的子协程抛出异常后,会直接取消父协程,子协程自己设置的 Handler 往往来不及响应。正确做法:在根协程或 SupervisorJob 的直接子协程上设置 Handler。

坑 3:在 Handler 回调里直接操作 UI

Handler 执行时所在线程由当时的拦截器决定,它本身不提供线程切换能力。要弹 Toast,先 withContext(Dispatchers.Main)

六、总结

CoroutineExceptionHandlerContinuationInterceptor 是协程上下文体系中两个正交、独立、各司其职的核心元素:没有继承关系,没有依赖关系,执行上也不重叠,却共同定义了协程最基础的"运行规则"和"安全边界"。

理解了它们的关系,你才算真正从"会用协程"走到"懂协程"。