从suspend字节码到 Retrofit 协程桥:挂起函数识别与恢复

我们每天都在 Retrofit 接口里写 suspend fun,但 JVM 字节码并不认识 suspend,Java 动态代理也无法直接看到 Kotlin 源码中的返回类型。那么 Retrofit 究竟凭什么判断这是挂起函数,又怎样把 OkHttp 的回调恢复成一个看起来顺序执行的返回值?

一句话结论:Kotlin 编译器把挂起函数改写为"末尾追加 Continuation、返回 Object"的 CPS 方法;Retrofit 通过 Java 反射识别最后一个 Continuation 参数,从其泛型中还原响应类型,再用 suspendCancellableCoroutine 把 OkHttp enqueue 的回调、异常与取消信号桥接回调用方协程。

本文验证基线

  • 字节码:Kotlin 2.2.10、JVM target 17,使用 javap -p -c -s 实际验证。
  • Retrofit:以 3.0.0 源码为主;对照 2.11.0 后,本文涉及的挂起函数识别与适配主链路一致。
  • Retrofit 从 2.6.0 起原生支持 suspend。内部类属于实现细节,不应在业务代码中直接依赖。

一、先看全链路:一行接口背后发生了什么

先从最常见的声明开始:

kotlin 复制代码
interface UserApi {
    @GET("users/{id}")
    suspend fun user(@Path("id") id: Long): User
}

从业务调用到结果恢复,核心路径如下:
#mermaid-svg-3DqE1XE1UkvMPsC2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3DqE1XE1UkvMPsC2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3DqE1XE1UkvMPsC2 .error-icon{fill:#552222;}#mermaid-svg-3DqE1XE1UkvMPsC2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3DqE1XE1UkvMPsC2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3DqE1XE1UkvMPsC2 .marker.cross{stroke:#333333;}#mermaid-svg-3DqE1XE1UkvMPsC2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3DqE1XE1UkvMPsC2 p{margin:0;}#mermaid-svg-3DqE1XE1UkvMPsC2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3DqE1XE1UkvMPsC2 .cluster-label text{fill:#333;}#mermaid-svg-3DqE1XE1UkvMPsC2 .cluster-label span{color:#333;}#mermaid-svg-3DqE1XE1UkvMPsC2 .cluster-label span p{background-color:transparent;}#mermaid-svg-3DqE1XE1UkvMPsC2 .label text,#mermaid-svg-3DqE1XE1UkvMPsC2 span{fill:#333;color:#333;}#mermaid-svg-3DqE1XE1UkvMPsC2 .node rect,#mermaid-svg-3DqE1XE1UkvMPsC2 .node circle,#mermaid-svg-3DqE1XE1UkvMPsC2 .node ellipse,#mermaid-svg-3DqE1XE1UkvMPsC2 .node polygon,#mermaid-svg-3DqE1XE1UkvMPsC2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3DqE1XE1UkvMPsC2 .rough-node .label text,#mermaid-svg-3DqE1XE1UkvMPsC2 .node .label text,#mermaid-svg-3DqE1XE1UkvMPsC2 .image-shape .label,#mermaid-svg-3DqE1XE1UkvMPsC2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-3DqE1XE1UkvMPsC2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3DqE1XE1UkvMPsC2 .rough-node .label,#mermaid-svg-3DqE1XE1UkvMPsC2 .node .label,#mermaid-svg-3DqE1XE1UkvMPsC2 .image-shape .label,#mermaid-svg-3DqE1XE1UkvMPsC2 .icon-shape .label{text-align:center;}#mermaid-svg-3DqE1XE1UkvMPsC2 .node.clickable{cursor:pointer;}#mermaid-svg-3DqE1XE1UkvMPsC2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3DqE1XE1UkvMPsC2 .arrowheadPath{fill:#333333;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3DqE1XE1UkvMPsC2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3DqE1XE1UkvMPsC2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3DqE1XE1UkvMPsC2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3DqE1XE1UkvMPsC2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3DqE1XE1UkvMPsC2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3DqE1XE1UkvMPsC2 .cluster text{fill:#333;}#mermaid-svg-3DqE1XE1UkvMPsC2 .cluster span{color:#333;}#mermaid-svg-3DqE1XE1UkvMPsC2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3DqE1XE1UkvMPsC2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3DqE1XE1UkvMPsC2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-3DqE1XE1UkvMPsC2 .icon-shape,#mermaid-svg-3DqE1XE1UkvMPsC2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3DqE1XE1UkvMPsC2 .icon-shape p,#mermaid-svg-3DqE1XE1UkvMPsC2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3DqE1XE1UkvMPsC2 .icon-shape rect,#mermaid-svg-3DqE1XE1UkvMPsC2 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3DqE1XE1UkvMPsC2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3DqE1XE1UkvMPsC2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3DqE1XE1UkvMPsC2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调用方:user
编译器:Continuation
Retrofit:动态代理
ServiceMethod 缓存
RequestFactory:识别 suspend
SuspendForBody 适配
KotlinExtensions.await
OkHttp Call.enqueue
网络回调
Continuation.resumeWith

这条链路中各对象的归属很重要:

角色 谁创建 谁持有 何时工作 结果交给谁
状态机与 Continuation Kotlin 编译器生成,协程启动器实例化 调用方协程及其父 Job 挂起、恢复时 原挂起函数的下一段状态
Retrofit 动态代理 Retrofit.create() 业务层的 UserApi 引用 每次接口调用 已解析的 ServiceMethod
ServiceMethod Retrofit 首次解析方法时创建 Retrofit 方法缓存 创建请求、选择适配方式 普通返回值或协程桥
OkHttpCall HttpServiceMethod.invoke() 本次请求链路 入队、解析响应、取消 Retrofit Callback
CancellableContinuation suspendCancellableCoroutine 当前协程 等待回调并处理取消 调用方状态机

这里没有 Activity 或 Fragment 级别的"魔法生命周期"。真正的生命周期拥有者是调用方的 CoroutineScopeJob。例如在 viewModelScope 中发起请求,ViewModel 清理时取消 Job,取消信号再沿协程桥传给 OkHttp Call.cancel()


二、suspend 到底编译成了什么

2.1 JVM 方法签名:返回类型为什么变成 Object

下面这段没有 Retrofit 依赖的纯 Kotlin 接口:

kotlin 复制代码
data class User(val name: String)

interface UserApi {
    suspend fun user(id: Long): User
}

使用 Kotlin 2.2.10 编译,再执行:

bash 复制代码
javap -classpath build/classes -p -s demo.UserApi

得到的关键签名是:

java 复制代码
public abstract java.lang.Object user(
    long,
    kotlin.coroutines.Continuation<? super demo.User>
);

descriptor: (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;

源码中的:

kotlin 复制代码
suspend fun user(id: Long): User

可以先粗略理解为下面的 CPS(Continuation-Passing Style,续体传递风格)形式:

java 复制代码
// 简化伪代码,不是编译器逐字生成的 Java 源码
Object user(long id, Continuation<? super User> continuation);

有两个关键变化:

  1. 参数列表末尾追加了一个 Continuation<? super User>
  2. JVM 返回类型统一变为 Object

为什么不是直接返回 User?因为一次调用可能出现两种结果:

  • 没有真正挂起 :方法同步完成,直接返回 User
  • 发生挂起 :方法返回特殊标记 COROUTINE_SUSPENDED,未来再通过 Continuation.resumeWith(...) 交付成功或失败。

所以 Object 是两条路径共同需要的容器,并不代表 Kotlin 源码中的业务类型丢失了。User 仍保存在最后一个 Continuation 的泛型参数中,这恰好给了 Retrofit 一个反射识别入口。

2.2 suspend 不等于"切到后台线程"

suspend 的语义是"允许函数暂停并在未来继续",不是线程调度指令。下面的函数仍然会阻塞当前线程:

kotlin 复制代码
suspend fun wrong() {
    Thread.sleep(3_000)
}

Retrofit 的挂起接口通常不需要额外包一层 withContext(Dispatchers.IO),是因为 Retrofit 最终调用了 OkHttp 的异步 enqueue(),而不是因为 suspend 自动选择了 IO 线程。

2.3 有挂起点时,函数体被改写为状态机

再看一个包含两个挂起点的函数:

kotlin 复制代码
class UserRepository {
    suspend fun load(id: Long): User {
        val token = loadToken()            // 挂起点 1
        val user = requestUser(id, token)  // 挂起点 2
        return user.copy(name = user.name.uppercase())
    }
}

实际编译后会生成类似 UserRepository$load$1 的类,并继承 ContinuationImpl。本次 javap 验证得到的核心字段如下:

java 复制代码
final class UserRepository$load$1 extends ContinuationImpl {
    long J$0;
    Object L$0;
    Object result;
    final UserRepository this$0;
    int label;

    public Object invokeSuspend(Object result);
}
字段 作用
label 记录恢复时应该跳到哪个状态
result 保存上一次挂起操作恢复的结果,成功值与异常都经 Result 语义传递
L$0 保存跨挂起点仍需使用的引用类型局部变量
J$0 保存跨挂起点仍需使用的 long 局部变量
this$0 保存外部 UserRepository 实例

并不是每出现一次 suspend 就必然生成一个同名状态机类。抽象接口只有降级后的方法签名,没有可改写的函数体;具体函数存在挂起点、并且需要保存恢复位置时,编译器才生成或复用相应状态机,最终形态还可能受到内联与后端优化影响。Retrofit 的 Service 接口正属于"只有签名"的情况,真正的调用方状态通常保存在 Repository、UseCase 或 ViewModel 的挂起函数中。

状态机主体可以简化成:

kotlin 复制代码
// 简化伪代码:用于理解 label 与 COROUTINE_SUSPENDED
fun load(id: Long, completion: Continuation<User>): Any? {
    val state = completion as? LoadContinuation
        ?: LoadContinuation(this, completion)

    when (state.label) {
        0 -> {
            throwOnFailure(state.result)
            state.savedId = id
            state.label = 1
            val token = loadToken(state)
            if (token === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
            state.result = token
        }

        1 -> {
            throwOnFailure(state.result)
            val token = state.result as String
            state.savedToken = token
            state.label = 2
            val user = requestUser(state.savedId, token, state)
            if (user === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
            state.result = user
        }

        2 -> throwOnFailure(state.result)
        else -> error("call to 'resume' before 'invoke' with coroutine")
    }

    val user = state.result as User
    return user.copy(name = user.name.uppercase())
}

源码事实 :真实字节码通过 tableswitch 分发 label,在每个挂起调用前写入下一个状态,并比较返回值是否为 COROUTINE_SUSPENDED。字段名称、溢出变量数量和优化结果可能随 Kotlin 编译器版本变化,但"Continuation + 状态标签 + 局部变量保存 + 恢复结果"这一模型稳定存在。

恢复过程可以这样理解:
"异步操作" "load 状态机" "调用方状态机" "异步操作" "load 状态机" "调用方状态机" #mermaid-svg-0DNdK75N0EFQa78t{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0DNdK75N0EFQa78t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0DNdK75N0EFQa78t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0DNdK75N0EFQa78t .error-icon{fill:#552222;}#mermaid-svg-0DNdK75N0EFQa78t .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0DNdK75N0EFQa78t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0DNdK75N0EFQa78t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0DNdK75N0EFQa78t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0DNdK75N0EFQa78t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0DNdK75N0EFQa78t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0DNdK75N0EFQa78t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0DNdK75N0EFQa78t .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0DNdK75N0EFQa78t .marker.cross{stroke:#333333;}#mermaid-svg-0DNdK75N0EFQa78t svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0DNdK75N0EFQa78t p{margin:0;}#mermaid-svg-0DNdK75N0EFQa78t .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0DNdK75N0EFQa78t text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0DNdK75N0EFQa78t .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0DNdK75N0EFQa78t .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0DNdK75N0EFQa78t .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0DNdK75N0EFQa78t .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0DNdK75N0EFQa78t #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0DNdK75N0EFQa78t .sequenceNumber{fill:white;}#mermaid-svg-0DNdK75N0EFQa78t #sequencenumber{fill:#333;}#mermaid-svg-0DNdK75N0EFQa78t #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0DNdK75N0EFQa78t .messageText{fill:#333;stroke:none;}#mermaid-svg-0DNdK75N0EFQa78t .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0DNdK75N0EFQa78t .labelText,#mermaid-svg-0DNdK75N0EFQa78t .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0DNdK75N0EFQa78t .loopText,#mermaid-svg-0DNdK75N0EFQa78t .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0DNdK75N0EFQa78t .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0DNdK75N0EFQa78t .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0DNdK75N0EFQa78t .noteText,#mermaid-svg-0DNdK75N0EFQa78t .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0DNdK75N0EFQa78t .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0DNdK75N0EFQa78t .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0DNdK75N0EFQa78t .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0DNdK75N0EFQa78t .actorPopupMenu{position:absolute;}#mermaid-svg-0DNdK75N0EFQa78t .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0DNdK75N0EFQa78t .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0DNdK75N0EFQa78t .actor-man circle,#mermaid-svg-0DNdK75N0EFQa78t line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0DNdK75N0EFQa78t :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} "调用并传入 Continuation" "保存局部变量并设置 label" "返回 COROUTINE_SUSPENDED" "暂时退出调用栈" "resumeWith 交付结果" "依据 label 继续执行" "返回最终 User"

线程并没有被保存在状态机里,保存的是"继续执行所需的数据与位置"。恢复到哪个线程,取决于 ContinuationInterceptor,通常就是协程上下文中的 CoroutineDispatcher


三、Retrofit 如何识别一个 suspend 方法

很多人会猜 Retrofit 读取了 Kotlin 的 @Metadata,或者通过 Kotlin 反射调用了 isSuspend。至少在 Retrofit 2.11.0/3.0.0 的这条主链路中,都不是。

它利用的就是上一节看到的 JVM 形态:最后一个参数是不是 kotlin.coroutines.Continuation

3.1 入口:Retrofit.create() 创建 Java 动态代理

Retrofit.create(UserApi::class.java) 返回的不是手写实现,而是 Proxy.newProxyInstance(...) 创建的 Java 动态代理。调用 api.user(1) 时,代理的 InvocationHandler 大致做两件事:

java 复制代码
// 简化伪代码
if (methodIsDefaultMethod) {
    return invokeDefaultMethod(...);
}
return loadServiceMethod(service, method).invoke(proxy, args);

第一次调用时,loadServiceMethod() 解析注解并构建 ServiceMethod;之后以 java.lang.reflect.Method 为 key 复用缓存,避免每次请求都重复做昂贵的反射解析。

注意:Kotlin 调用点已经把当前 Continuation 作为隐藏的最后一个实参传入,所以动态代理收到的 args 实际类似:

text 复制代码
[1L, currentContinuation]

3.2 RequestFactory:只检查最后一个参数

RequestFactory.Builder 遍历方法参数时,会把"是否为最后一个参数"传给参数解析逻辑:

java 复制代码
// 基于 Retrofit 3.0.0 的简化源码
for (int p = 0, last = parameterCount - 1; p < parameterCount; p++) {
    handlers[p] = parseParameter(
        p,
        parameterTypes[p],
        parameterAnnotations[p],
        p == last
    );
}

当某个参数没有 Retrofit 注解,并且它正好是最后一个参数时,代码会检查:

java 复制代码
// 简化伪代码
if (allowContinuation
        && getRawType(parameterType) == Continuation.class) {
    isKotlinSuspendFunction = true;
    return null;
}

这一步体现了两个约束:

  • Continuation 必须位于参数列表末尾,这与 Kotlin 编译器约定一致。
  • 它不参与 @Path@Query@Body 等请求参数组装,因此对应的 ParameterHandlernull

真正构建 HTTP 请求时,如果是挂起函数,参数数量会先减一,最后一个 Continuation 不会被加入 URL、Header 或 Body:

java 复制代码
// 简化伪代码
if (isKotlinSuspendFunction) {
    argumentCount--;
}
for (int p = 0; p < argumentCount; p++) {
    parameterHandlers[p].apply(requestBuilder, args[p]);
}

3.3 HttpServiceMethod:从 Continuation 还原 User

反射看到的方法返回类型只是 Object,直接使用它显然找不到 Converter<ResponseBody, User>。Retrofit 转而读取最后一个参数:

java 复制代码
Type[] types = method.getGenericParameterTypes();
Type continuationType = types[types.length - 1];

// Continuation<? super User> -> User
Type responseType = getParameterLowerBound(
    0,
    (ParameterizedType) continuationType
);

为什么读取 lower bound?因为编译后的泛型是:

java 复制代码
Continuation<? super User>

其中 ? super User 的下界才是业务返回类型 User

接着 Retrofit 会根据还原出的类型选择两条路径:
#mermaid-svg-kxSbPAViM5TsN5kQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kxSbPAViM5TsN5kQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kxSbPAViM5TsN5kQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kxSbPAViM5TsN5kQ .error-icon{fill:#552222;}#mermaid-svg-kxSbPAViM5TsN5kQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kxSbPAViM5TsN5kQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kxSbPAViM5TsN5kQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kxSbPAViM5TsN5kQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kxSbPAViM5TsN5kQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kxSbPAViM5TsN5kQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kxSbPAViM5TsN5kQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kxSbPAViM5TsN5kQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kxSbPAViM5TsN5kQ .marker.cross{stroke:#333333;}#mermaid-svg-kxSbPAViM5TsN5kQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kxSbPAViM5TsN5kQ p{margin:0;}#mermaid-svg-kxSbPAViM5TsN5kQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kxSbPAViM5TsN5kQ .cluster-label text{fill:#333;}#mermaid-svg-kxSbPAViM5TsN5kQ .cluster-label span{color:#333;}#mermaid-svg-kxSbPAViM5TsN5kQ .cluster-label span p{background-color:transparent;}#mermaid-svg-kxSbPAViM5TsN5kQ .label text,#mermaid-svg-kxSbPAViM5TsN5kQ span{fill:#333;color:#333;}#mermaid-svg-kxSbPAViM5TsN5kQ .node rect,#mermaid-svg-kxSbPAViM5TsN5kQ .node circle,#mermaid-svg-kxSbPAViM5TsN5kQ .node ellipse,#mermaid-svg-kxSbPAViM5TsN5kQ .node polygon,#mermaid-svg-kxSbPAViM5TsN5kQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kxSbPAViM5TsN5kQ .rough-node .label text,#mermaid-svg-kxSbPAViM5TsN5kQ .node .label text,#mermaid-svg-kxSbPAViM5TsN5kQ .image-shape .label,#mermaid-svg-kxSbPAViM5TsN5kQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-kxSbPAViM5TsN5kQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kxSbPAViM5TsN5kQ .rough-node .label,#mermaid-svg-kxSbPAViM5TsN5kQ .node .label,#mermaid-svg-kxSbPAViM5TsN5kQ .image-shape .label,#mermaid-svg-kxSbPAViM5TsN5kQ .icon-shape .label{text-align:center;}#mermaid-svg-kxSbPAViM5TsN5kQ .node.clickable{cursor:pointer;}#mermaid-svg-kxSbPAViM5TsN5kQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kxSbPAViM5TsN5kQ .arrowheadPath{fill:#333333;}#mermaid-svg-kxSbPAViM5TsN5kQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kxSbPAViM5TsN5kQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kxSbPAViM5TsN5kQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kxSbPAViM5TsN5kQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kxSbPAViM5TsN5kQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kxSbPAViM5TsN5kQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kxSbPAViM5TsN5kQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kxSbPAViM5TsN5kQ .cluster text{fill:#333;}#mermaid-svg-kxSbPAViM5TsN5kQ .cluster span{color:#333;}#mermaid-svg-kxSbPAViM5TsN5kQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kxSbPAViM5TsN5kQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kxSbPAViM5TsN5kQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-kxSbPAViM5TsN5kQ .icon-shape,#mermaid-svg-kxSbPAViM5TsN5kQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kxSbPAViM5TsN5kQ .icon-shape p,#mermaid-svg-kxSbPAViM5TsN5kQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kxSbPAViM5TsN5kQ .icon-shape rect,#mermaid-svg-kxSbPAViM5TsN5kQ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kxSbPAViM5TsN5kQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kxSbPAViM5TsN5kQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kxSbPAViM5TsN5kQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



反射 Method
最后参数 Continuation
提取泛型下界 T
T 是否为 Response
解包 Response Body 类型
SuspendForResponse
SuspendForBody
T 是否为 Unit
awaitUnit
await Body

  • suspend fun user(): User 选择 SuspendForBody<User>
  • suspend fun user(): Response<User> 选择 SuspendForResponse<User>
  • suspend fun ping(): UnitawaitUnit 的特殊处理。

还有一个看似绕弯的设计:源码会临时构造 Call<User> 这个类型,再交给 CallAdapter.Factory

java 复制代码
Type adapterType = new ParameterizedTypeImpl(
    null,
    Call.class,
    responseType
);

原因是挂起函数虽然对外返回 User,内部仍需要先得到一个可执行、可取消的 Call<User>,然后才能桥接为协程。原有的 Converter 与 CallAdapter 体系因此得以复用,无须另写一套网络管线。

源码还会给挂起分支补上内部的 SkipCallbackExecutor 标记。默认 CallAdapter 看到它后不会再用 Retrofit 的 callbackExecutor 包装 Call,避免先经过 Android 主线程回调执行器、再由协程 Dispatcher 二次调度。网络回调负责恢复 Continuation,恢复后的执行线程则由调用方协程上下文决定。

常见误解 :Retrofit 不是通过方法名、注解或源码中的 suspend 关键字识别挂起函数,也不要求额外安装早期第三方 CoroutineCallAdapterFactory。原生挂起支持从 Retrofit 2.6.0 已经进入主库。


四、调用时:SuspendForBody 如何接住 Continuation

解析阶段完成后,本次方法对应的 ServiceMethod 已经缓存。每次调用的执行路径如下:
#mermaid-svg-5EKAOiSPdPYGd8ea{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5EKAOiSPdPYGd8ea .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5EKAOiSPdPYGd8ea .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5EKAOiSPdPYGd8ea .error-icon{fill:#552222;}#mermaid-svg-5EKAOiSPdPYGd8ea .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5EKAOiSPdPYGd8ea .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5EKAOiSPdPYGd8ea .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5EKAOiSPdPYGd8ea .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5EKAOiSPdPYGd8ea .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5EKAOiSPdPYGd8ea .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5EKAOiSPdPYGd8ea .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5EKAOiSPdPYGd8ea .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5EKAOiSPdPYGd8ea .marker.cross{stroke:#333333;}#mermaid-svg-5EKAOiSPdPYGd8ea svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5EKAOiSPdPYGd8ea p{margin:0;}#mermaid-svg-5EKAOiSPdPYGd8ea .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5EKAOiSPdPYGd8ea .cluster-label text{fill:#333;}#mermaid-svg-5EKAOiSPdPYGd8ea .cluster-label span{color:#333;}#mermaid-svg-5EKAOiSPdPYGd8ea .cluster-label span p{background-color:transparent;}#mermaid-svg-5EKAOiSPdPYGd8ea .label text,#mermaid-svg-5EKAOiSPdPYGd8ea span{fill:#333;color:#333;}#mermaid-svg-5EKAOiSPdPYGd8ea .node rect,#mermaid-svg-5EKAOiSPdPYGd8ea .node circle,#mermaid-svg-5EKAOiSPdPYGd8ea .node ellipse,#mermaid-svg-5EKAOiSPdPYGd8ea .node polygon,#mermaid-svg-5EKAOiSPdPYGd8ea .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5EKAOiSPdPYGd8ea .rough-node .label text,#mermaid-svg-5EKAOiSPdPYGd8ea .node .label text,#mermaid-svg-5EKAOiSPdPYGd8ea .image-shape .label,#mermaid-svg-5EKAOiSPdPYGd8ea .icon-shape .label{text-anchor:middle;}#mermaid-svg-5EKAOiSPdPYGd8ea .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5EKAOiSPdPYGd8ea .rough-node .label,#mermaid-svg-5EKAOiSPdPYGd8ea .node .label,#mermaid-svg-5EKAOiSPdPYGd8ea .image-shape .label,#mermaid-svg-5EKAOiSPdPYGd8ea .icon-shape .label{text-align:center;}#mermaid-svg-5EKAOiSPdPYGd8ea .node.clickable{cursor:pointer;}#mermaid-svg-5EKAOiSPdPYGd8ea .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5EKAOiSPdPYGd8ea .arrowheadPath{fill:#333333;}#mermaid-svg-5EKAOiSPdPYGd8ea .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5EKAOiSPdPYGd8ea .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5EKAOiSPdPYGd8ea .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5EKAOiSPdPYGd8ea .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5EKAOiSPdPYGd8ea .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5EKAOiSPdPYGd8ea .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5EKAOiSPdPYGd8ea .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5EKAOiSPdPYGd8ea .cluster text{fill:#333;}#mermaid-svg-5EKAOiSPdPYGd8ea .cluster span{color:#333;}#mermaid-svg-5EKAOiSPdPYGd8ea div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5EKAOiSPdPYGd8ea .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5EKAOiSPdPYGd8ea rect.text{fill:none;stroke-width:0;}#mermaid-svg-5EKAOiSPdPYGd8ea .icon-shape,#mermaid-svg-5EKAOiSPdPYGd8ea .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5EKAOiSPdPYGd8ea .icon-shape p,#mermaid-svg-5EKAOiSPdPYGd8ea .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5EKAOiSPdPYGd8ea .icon-shape rect,#mermaid-svg-5EKAOiSPdPYGd8ea .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5EKAOiSPdPYGd8ea .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5EKAOiSPdPYGd8ea .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5EKAOiSPdPYGd8ea :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ServiceMethod.invoke
创建 OkHttpCall
CallAdapter.adapt
取 args 最后一项
KotlinExtensions.await
OkHttp 异步队列

HttpServiceMethod.invoke() 先创建包装底层请求的 OkHttpCall,然后交给子类适配:

java 复制代码
// 简化伪代码
Object invoke(Object instance, Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(
        requestFactory,
        instance,
        args,
        callFactory,
        responseConverter
    );
    return adapt(call, args);
}

SuspendForBody 再从实参数组末尾拿到编译器传入的 Continuation:

java 复制代码
// 简化伪代码
Call<ResponseT> adaptedCall = callAdapter.adapt(call);
Continuation<ResponseT> continuation =
    (Continuation<ResponseT>) args[args.length - 1];

return KotlinExtensions.await(adaptedCall, continuation);

Kotlin 的 suspend fun Call<T>.await(): T 在 JVM 层同样被降级成接收 Continuation、返回 Object 的静态方法,所以 Java 编写的 Retrofit 内部代码可以直接调用它。


五、真正的桥:suspendCancellableCoroutine

Retrofit 的 KotlinExtensions.await() 核心可以整理为:

kotlin 复制代码
// 基于 Retrofit 3.0.0 的简化代码
suspend fun <T : Any> Call<T>.await(): T =
    suspendCancellableCoroutine { continuation ->
        continuation.invokeOnCancellation {
            cancel()
        }

        enqueue(object : Callback<T> {
            override fun onResponse(call: Call<T>, response: Response<T>) {
                if (!response.isSuccessful) {
                    continuation.resumeWithException(HttpException(response))
                    return
                }

                val body = response.body()
                if (body == null) {
                    continuation.resumeWithException(
                        KotlinNullPointerException("Response body was null")
                    )
                } else {
                    continuation.resume(body)
                }
            }

            override fun onFailure(call: Call<T>, error: Throwable) {
                continuation.resumeWithException(error)
            }
        })
    }

这里完成了三种桥接。

5.1 回调桥接为挂起与恢复

suspendCancellableCoroutine 先把当前协程包装成可取消的 Continuation。请求未完成时函数返回 COROUTINE_SUSPENDED,当前线程可以继续处理其他任务;OkHttp 回调到达后再调用 resumeresumeWithException,状态机从原挂起点之后继续。

5.2 协程取消桥接为网络取消

kotlin 复制代码
continuation.invokeOnCancellation {
    call.cancel()
}

因此下面的取消链是成立的:
#mermaid-svg-LX9HIJTlje7kMGmk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LX9HIJTlje7kMGmk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LX9HIJTlje7kMGmk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LX9HIJTlje7kMGmk .error-icon{fill:#552222;}#mermaid-svg-LX9HIJTlje7kMGmk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LX9HIJTlje7kMGmk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LX9HIJTlje7kMGmk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LX9HIJTlje7kMGmk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LX9HIJTlje7kMGmk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LX9HIJTlje7kMGmk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LX9HIJTlje7kMGmk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LX9HIJTlje7kMGmk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LX9HIJTlje7kMGmk .marker.cross{stroke:#333333;}#mermaid-svg-LX9HIJTlje7kMGmk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LX9HIJTlje7kMGmk p{margin:0;}#mermaid-svg-LX9HIJTlje7kMGmk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-LX9HIJTlje7kMGmk .cluster-label text{fill:#333;}#mermaid-svg-LX9HIJTlje7kMGmk .cluster-label span{color:#333;}#mermaid-svg-LX9HIJTlje7kMGmk .cluster-label span p{background-color:transparent;}#mermaid-svg-LX9HIJTlje7kMGmk .label text,#mermaid-svg-LX9HIJTlje7kMGmk span{fill:#333;color:#333;}#mermaid-svg-LX9HIJTlje7kMGmk .node rect,#mermaid-svg-LX9HIJTlje7kMGmk .node circle,#mermaid-svg-LX9HIJTlje7kMGmk .node ellipse,#mermaid-svg-LX9HIJTlje7kMGmk .node polygon,#mermaid-svg-LX9HIJTlje7kMGmk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LX9HIJTlje7kMGmk .rough-node .label text,#mermaid-svg-LX9HIJTlje7kMGmk .node .label text,#mermaid-svg-LX9HIJTlje7kMGmk .image-shape .label,#mermaid-svg-LX9HIJTlje7kMGmk .icon-shape .label{text-anchor:middle;}#mermaid-svg-LX9HIJTlje7kMGmk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LX9HIJTlje7kMGmk .rough-node .label,#mermaid-svg-LX9HIJTlje7kMGmk .node .label,#mermaid-svg-LX9HIJTlje7kMGmk .image-shape .label,#mermaid-svg-LX9HIJTlje7kMGmk .icon-shape .label{text-align:center;}#mermaid-svg-LX9HIJTlje7kMGmk .node.clickable{cursor:pointer;}#mermaid-svg-LX9HIJTlje7kMGmk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LX9HIJTlje7kMGmk .arrowheadPath{fill:#333333;}#mermaid-svg-LX9HIJTlje7kMGmk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LX9HIJTlje7kMGmk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LX9HIJTlje7kMGmk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LX9HIJTlje7kMGmk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LX9HIJTlje7kMGmk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LX9HIJTlje7kMGmk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LX9HIJTlje7kMGmk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LX9HIJTlje7kMGmk .cluster text{fill:#333;}#mermaid-svg-LX9HIJTlje7kMGmk .cluster span{color:#333;}#mermaid-svg-LX9HIJTlje7kMGmk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LX9HIJTlje7kMGmk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LX9HIJTlje7kMGmk rect.text{fill:none;stroke-width:0;}#mermaid-svg-LX9HIJTlje7kMGmk .icon-shape,#mermaid-svg-LX9HIJTlje7kMGmk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LX9HIJTlje7kMGmk .icon-shape p,#mermaid-svg-LX9HIJTlje7kMGmk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LX9HIJTlje7kMGmk .icon-shape rect,#mermaid-svg-LX9HIJTlje7kMGmk .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LX9HIJTlje7kMGmk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LX9HIJTlje7kMGmk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LX9HIJTlje7kMGmk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ViewModel 清理
viewModelScope Job 取消
Continuation 取消
invokeOnCancellation
OkHttp Call.cancel
结束或中断网络工作

"谁拥有生命周期"的答案不是 Retrofit,而是启动请求的作用域:

kotlin 复制代码
class UserViewModel(
    private val api: UserApi
) : ViewModel() {

    fun refresh(id: Long) {
        viewModelScope.launch {
            val user = api.user(id)
            // 更新 UI State
        }
    }
}

ViewModel 被清理时,viewModelScope 取消其子协程;Retrofit 的取消处理器随后调用 Call.cancel()。如果把请求放进一个永不取消的全局作用域,Retrofit 不会替业务层猜测生命周期。

5.3 网络异常桥接为协程异常

不同声明的结果语义并不相同:

接口返回类型 HTTP 2xx HTTP 4xx/5xx 网络失败 适合场景
T 返回非空 Body HttpException 抛底层异常,常见为 IOException 只关心成功 Body
Response<T> 返回完整 Response 仍返回完整 Response 抛底层异常 需要状态码、Header、错误 Body
Unit 返回 Unit HttpException 抛底层异常 无响应体接口
Call<T> 由调用者执行与处理 由调用者处理 由调用者处理 旧式回调或需要显式控制请求对象

SuspendForBody 中还存在一层 try/catch + suspendAndThrow。它处理的是动态代理边界上的同步异常:Java Proxy 无法直接抛出接口未声明的 checked exception,否则可能包装成 UndeclaredThrowableException。Retrofit 强制先发生一次挂起,再通过 Continuation 交付异常,从而绕过这项 Java 代理限制。VirtualMachineErrorThreadDeathLinkageError 等致命错误不会被吞掉。


六、线程到底在哪里切换

完整理解 Retrofit 协程支持,需要把三个概念分开:

层级 负责什么 不负责什么
suspend/状态机 保存执行位置,允许稍后恢复 不自动创建线程,不保证后台执行
OkHttp Dispatcher 调度异步 HTTP 工作,维护并发请求队列 不拥有业务协程生命周期
协程 CoroutineDispatcher 决定 Continuation 恢复后在哪个线程执行 不替 Retrofit 执行网络协议栈

典型情况下:

  1. 主线程中的 viewModelScope.launch 调用挂起接口。
  2. Retrofit 调用 OkHttp enqueue(),函数挂起,主线程不被网络等待占用。
  3. OkHttp 在自己的工作线程执行网络 I/O,并在响应路径中完成 Body 转换。
  4. continuation.resume(...) 提交恢复任务。
  5. Dispatchers.Main.immediate 等调用方上下文决定业务代码从哪里继续。

所以通常没有必要这样写:

kotlin 复制代码
// 对 Retrofit 原生 suspend 接口通常是冗余的
withContext(Dispatchers.IO) {
    api.user(id)
}

但如果挂起函数内部执行的是 JDBC、文件 I/O 或某个同步阻塞 SDK,仍然需要由你明确切到 Dispatchers.IO。判断标准是底层实现是否异步,而不是函数签名是否带 suspend


七、Converter、CallAdapter、OkHttp 各守哪条边界

从网络字节到业务对象,中间还有三层不能混为一谈:
#mermaid-svg-C9m4jCedP7X8JkWo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C9m4jCedP7X8JkWo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C9m4jCedP7X8JkWo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C9m4jCedP7X8JkWo .error-icon{fill:#552222;}#mermaid-svg-C9m4jCedP7X8JkWo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C9m4jCedP7X8JkWo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C9m4jCedP7X8JkWo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C9m4jCedP7X8JkWo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C9m4jCedP7X8JkWo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C9m4jCedP7X8JkWo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C9m4jCedP7X8JkWo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C9m4jCedP7X8JkWo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C9m4jCedP7X8JkWo .marker.cross{stroke:#333333;}#mermaid-svg-C9m4jCedP7X8JkWo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C9m4jCedP7X8JkWo p{margin:0;}#mermaid-svg-C9m4jCedP7X8JkWo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C9m4jCedP7X8JkWo .cluster-label text{fill:#333;}#mermaid-svg-C9m4jCedP7X8JkWo .cluster-label span{color:#333;}#mermaid-svg-C9m4jCedP7X8JkWo .cluster-label span p{background-color:transparent;}#mermaid-svg-C9m4jCedP7X8JkWo .label text,#mermaid-svg-C9m4jCedP7X8JkWo span{fill:#333;color:#333;}#mermaid-svg-C9m4jCedP7X8JkWo .node rect,#mermaid-svg-C9m4jCedP7X8JkWo .node circle,#mermaid-svg-C9m4jCedP7X8JkWo .node ellipse,#mermaid-svg-C9m4jCedP7X8JkWo .node polygon,#mermaid-svg-C9m4jCedP7X8JkWo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C9m4jCedP7X8JkWo .rough-node .label text,#mermaid-svg-C9m4jCedP7X8JkWo .node .label text,#mermaid-svg-C9m4jCedP7X8JkWo .image-shape .label,#mermaid-svg-C9m4jCedP7X8JkWo .icon-shape .label{text-anchor:middle;}#mermaid-svg-C9m4jCedP7X8JkWo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C9m4jCedP7X8JkWo .rough-node .label,#mermaid-svg-C9m4jCedP7X8JkWo .node .label,#mermaid-svg-C9m4jCedP7X8JkWo .image-shape .label,#mermaid-svg-C9m4jCedP7X8JkWo .icon-shape .label{text-align:center;}#mermaid-svg-C9m4jCedP7X8JkWo .node.clickable{cursor:pointer;}#mermaid-svg-C9m4jCedP7X8JkWo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C9m4jCedP7X8JkWo .arrowheadPath{fill:#333333;}#mermaid-svg-C9m4jCedP7X8JkWo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C9m4jCedP7X8JkWo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C9m4jCedP7X8JkWo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C9m4jCedP7X8JkWo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C9m4jCedP7X8JkWo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C9m4jCedP7X8JkWo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C9m4jCedP7X8JkWo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C9m4jCedP7X8JkWo .cluster text{fill:#333;}#mermaid-svg-C9m4jCedP7X8JkWo .cluster span{color:#333;}#mermaid-svg-C9m4jCedP7X8JkWo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C9m4jCedP7X8JkWo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C9m4jCedP7X8JkWo rect.text{fill:none;stroke-width:0;}#mermaid-svg-C9m4jCedP7X8JkWo .icon-shape,#mermaid-svg-C9m4jCedP7X8JkWo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C9m4jCedP7X8JkWo .icon-shape p,#mermaid-svg-C9m4jCedP7X8JkWo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C9m4jCedP7X8JkWo .icon-shape rect,#mermaid-svg-C9m4jCedP7X8JkWo .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C9m4jCedP7X8JkWo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C9m4jCedP7X8JkWo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C9m4jCedP7X8JkWo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Service 接口
ServiceMethod 模型
Retrofit Call
OkHttp 请求
ResponseBody 字节
Converter 转为 T
await 恢复协程

  • CallAdapter 回答"调用结果以什么外观暴露"。挂起函数内部先适配成 Call<T>,再桥接为协程结果。
  • Converter 回答"ResponseBody 如何变成 User"。例如 Moshi、Gson 或 Kotlin Serialization Converter。
  • OkHttp 回答"请求如何排队、执行、复用连接、缓存与取消"。HTTP 缓存位于 OkHttp 边界,不是 Continuation 状态机的一部分。

因此"Retrofit 识别 suspend"只改变结果交付方式,不会改变 JSON 解析器、缓存规则、连接池或拦截器的基本职责。


八、自己写一个最小协程桥,快速验证原理

下面的例子保留了最能说明问题的"回调、挂起、恢复、取消"四件事:

kotlin 复制代码
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

interface SimpleCall<T> {
    fun enqueue(callback: Callback<T>)
    fun cancel()
}

interface Callback<T> {
    fun onSuccess(value: T)
    fun onFailure(error: Throwable)
}

suspend fun <T> SimpleCall<T>.await(): T =
    suspendCancellableCoroutine { continuation ->
        continuation.invokeOnCancellation {
            cancel()
        }

        enqueue(object : Callback<T> {
            override fun onSuccess(value: T) {
                continuation.resume(value)
            }

            override fun onFailure(error: Throwable) {
                continuation.resumeWithException(error)
            }
        })
    }

这个实现可以用于学习,但不是生产级 Retrofit 替代品。它刻意省略了:HTTP 状态码与错误 Body、回调并发竞态、重复恢复防护、同步异常、空值语义、Converter、CallAdapter、请求克隆、拦截器、缓存以及 Java 动态代理方法缓存。

如果只想观察 Retrofit 用到的"识别形态",可以对 Java 反射方法做一个教学检测:

kotlin 复制代码
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import java.lang.reflect.WildcardType
import kotlin.coroutines.Continuation

// 简化实现:仅覆盖 Continuation<? super T> 的典型 JVM 形态
fun Method.suspendResultTypeOrNull(): java.lang.reflect.Type? {
    val last = genericParameterTypes.lastOrNull() as? ParameterizedType
        ?: return null
    if (last.rawType != Continuation::class.java) return null

    val wildcard = last.actualTypeArguments[0] as? WildcardType
        ?: return null
    return wildcard.lowerBounds.firstOrNull()
}

生产代码不应复制这段函数来替代 Retrofit:泛型、平台类型、混淆、编译器版本和边界类型都需要完整处理。


九、最容易踩的坑

9.1 在挂起函数中返回 Call<T>

kotlin 复制代码
// 错误设计
@GET("users/{id}")
suspend fun user(@Path("id") id: Long): Call<User>

suspend 已经表达异步结果,再返回 Call 会形成两层异步容器。Retrofit 会拒绝这种声明,应改为:

kotlin 复制代码
suspend fun user(id: Long): User
// 或者
suspend fun user(id: Long): Response<User>

9.2 把 CancellationException 当普通异常吞掉

kotlin 复制代码
// 有问题:可能破坏结构化并发中的取消传播
try {
    api.user(id)
} catch (t: Throwable) {
    emitError(t)
}

建议只捕获需要转换的业务异常,或者显式重新抛出取消:

kotlin 复制代码
try {
    api.user(id)
} catch (cancel: CancellationException) {
    throw cancel
} catch (http: HttpException) {
    // 映射 HTTP 错误
} catch (io: IOException) {
    // 映射网络错误
}

9.3 认为 Retrofit 自动绑定页面生命周期

Retrofit 只知道本次 Call 与传入的 Continuation,不知道页面是否可见。使用 viewModelScopelifecycleScope 或应用级作用域,是业务架构的所有权决策。

9.4 对 204 和可空 Body 缺少明确策略

在 Retrofit 3.0.0 源码中,HttpServiceMethod 仍有"从 Kotlin Metadata 判断可空返回类型"的 TODO;对应 bodyNullable 测试也被标记为尚未工作。因此不要把 suspend fun result(): T? 当成所有空响应场景都稳定无歧义的契约。

更稳妥的方式是:

  • 无响应体接口声明为 Unit
  • 需要区分 204、错误码和 Body 时返回 Response<T>,在 Repository 明确映射。
  • 在领域层使用 sealed interface ApiResult 表达成功、空数据、HTTP 错误和网络错误。

这是版本相关实现细节,升级 Retrofit 后应结合目标版本源码与测试重新确认。

9.5 为原生 suspend 额外添加协程 CallAdapter

Retrofit 2.6.0 之后,普通 suspend fun ...: T 已由主库支持。RxJava、Result 包装或自定义响应容器仍可能需要各自的 Adapter,但那是返回模型设计,不是让 Retrofit "认识 suspend"的必要条件。


十、排查方法:从源码声明一路看到字节码

当自定义代理、注解处理器、混淆或 Retrofit 返回类型出现问题时,可以按下面的顺序排查。

10.1 查看 Kotlin Bytecode

Android Studio 中打开 Kotlin 文件,使用 Tools → Kotlin → Show Kotlin Bytecode ,再点击 Decompile。重点观察:

  • 方法返回描述符是否为 Ljava/lang/Object;
  • 最后一个参数是否为 kotlin.coroutines.Continuation
  • 具体函数是否生成 ContinuationImpl 子类。
  • labelresultL$0 等状态字段是否出现。

10.2 使用 javap

bash 复制代码
javap -classpath build/tmp/kotlin-classes/debug \
  -p -c -s 'com.example.UserRepository$load$1'

常用参数:

  • -p:显示 private 成员。
  • -c:显示 JVM 指令。
  • -s:显示方法描述符。
  • -v:需要进一步查看泛型签名、注解和局部变量表时使用。

10.3 在 Retrofit 入口打断点

按链路依次观察:

  1. Retrofit.create() 中的 InvocationHandler.invoke()
  2. Retrofit.loadServiceMethod() 是否命中缓存。
  3. RequestFactory.Builder.parseParameter() 是否将 isKotlinSuspendFunction 设为 true
  4. HttpServiceMethod.parseAnnotations() 还原出的 responseType
  5. SuspendForBody.adapt()SuspendForResponse.adapt()
  6. KotlinExtensions.await() 中的 enqueue()resume() 与取消回调。

如果断点看到的方法参数是 (@Path id, Continuation),说明 Kotlin 降级阶段正常;如果响应类型错误,应继续检查 Continuation 的泛型签名、R8 规则、Converter 和自定义 CallAdapter。


十一、与 Call<T>suspend TFlow<T> 怎么选

形式 结果次数 启动方式 取消所有者 推荐用途
Call<T> 一次 executeenqueue 调用者显式 cancel Java、旧代码、需要直接控制请求
suspend fun(): T 一次 调用即执行,到挂起点等待 调用协程的 Job Repository 中的一次性请求
suspend fun(): Response<T> 一次 同上 调用协程的 Job 需要状态码、Header、错误 Body
Flow<T> 零到多次 默认收集时启动 收集协程的 Job 缓存观察、轮询、数据库与网络组合

Retrofit 原生挂起调用本质上是"一次请求、一次结果"。如果业务需要流式状态,可以在 Repository 中把请求、缓存与重试组织成 Flow,但不要因为 Flow 流行就把每个单次 HTTP 请求机械包装成流。


十二、面试与复盘:把原理压缩成 8 个问题

1. JVM 认识 suspend 关键字吗?

不认识。Kotlin 编译器把它降级为末尾带 Continuation、返回 Object 的普通 JVM 方法。

2. 为什么返回值是 Object?

它既可能是同步完成的业务值,也可能是 COROUTINE_SUSPENDED 标记。

3. 状态机保存了什么?

恢复位置 label、上次恢复结果 result、跨挂起点使用的局部变量,以及外部实例等。

4. Retrofit 怎么识别 suspend?

解析最后一个参数的原始类型是否为 Continuation,不依赖方法名,也不靠 suspend 注解。

5. Retrofit 怎么知道真正的返回类型是 User?

Continuation<? super User> 的泛型下界取出 User

6. 为什么内部仍然创建 Call<User>

为了复用 Retrofit 的 CallAdapter、Converter 与 OkHttpCall 执行管线,再在末端桥接成协程。

7. 协程取消为什么能取消网络?

suspendCancellableCoroutine 的取消处理器调用 Call.cancel(),由调用方 Job 发起取消传播。

8. suspend 会自动切换到 IO 线程吗?

不会。Retrofit 不阻塞主线程来自 OkHttp 的异步 enqueue();恢复线程由协程上下文决定。


十三、设计检查清单

  • 单次请求优先声明 suspend fun ...: TResponse<T>,不要返回 Call<T>
  • viewModelScopelifecycleScope 或明确的应用作用域拥有请求生命周期。
  • 不吞掉 CancellationException
  • 分别处理 HTTP 错误、网络错误、解析错误与业务错误。
  • 对 204、空 Body、可空字段建立明确契约。
  • 不为 Retrofit 原生异步调用机械添加 withContext(Dispatchers.IO)
  • 自定义 CallAdapter 时,确认输入类型实际是 Retrofit 合成的 Call<T>
  • 升级 Kotlin、Retrofit 或 R8 后,用 Bytecode、断点与 MockWebServer 回归取消及异常路径。

十四、总结

理解 Retrofit 的协程支持,可以拆成两层:

第一层是 Kotlin 编译器。它通过 CPS 变换,把 suspend fun 变成携带 Continuation 的 JVM 方法,并为包含挂起点的具体函数生成状态机。第二层是 Retrofit。它利用这个稳定的 JVM 形态识别挂起接口,从 Continuation 泛型还原响应类型,继续复用 CallAdapter、Converter 与 OkHttp 执行链,最后由 suspendCancellableCoroutine 完成回调、异常和取消的双向桥接。

掌握这条链路后,"挂起函数为什么能顺序写异步代码""Retrofit 为什么知道返回类型""页面退出为什么能取消请求"就不再是三个孤立知识点,而是同一条 Continuation 数据流上的不同阶段。


参考资料

  1. Kotlin Specification:Asynchronous programming with coroutines
  2. Kotlin API:Continuation
  3. kotlinx.coroutines API:suspendCancellableCoroutine
  4. Retrofit 3.0.0:Retrofit.java
  5. Retrofit 3.0.0:RequestFactory.java
  6. Retrofit 3.0.0:HttpServiceMethod.java
  7. Retrofit 3.0.0:KotlinExtensions.kt
  8. Retrofit CHANGELOG:2.6.0 原生支持 suspend
  9. Retrofit 3.0.0:KotlinSuspendTest.kt
相关推荐
杉氧2 小时前
RN 性能调优指南:重渲染(Re-renders)控制与长列表(FlatList)优化
android·前端·react native
Coffeeee2 小时前
claude-video 一个让你的Agent拥有看视频能力的Skill
android·人工智能·aigc
菜鸟~noob2334 小时前
【电子战】第07篇:多普勒测向【含matlab代码】
android·开发语言·matlab
样子20184 小时前
Js 之根据白名单过滤 HTML(防止 XSS 攻击)
android·前端·javascript·html·xss
InsightCore4 小时前
别再往 Skill 里塞一切:我们如何重新思考 AI Debug Engineer
android·debug
Kapaseker4 小时前
写过 4000 行 ViewModel 后,我开始这样拆 Compose 页面
android·kotlin
恋猫de小郭4 小时前
ADB Wi-Fi 2.0 ,Android 17 把无线调试的连接链路重新做了一遍
android·前端·flutter
亿是守候 & 亿是承诺5 小时前
第二章 控制系统的数学模型
android
Carson带你学Android5 小时前
ADK for Kotlin:Google 官方 AI Agent 教程来了
android·agent·ai编程