我们每天都在 Retrofit 接口里写 suspend fun,但 JVM 字节码并不认识 suspend,Java 动态代理也无法直接看到 Kotlin 源码中的返回类型。那么 Retrofit 究竟凭什么判断这是挂起函数,又怎样把 OkHttp 的回调恢复成一个看起来顺序执行的返回值?
一句话结论:Kotlin 编译器把挂起函数改写为"末尾追加 Continuation、返回 Object"的 CPS 方法;Retrofit 通过 Java 反射识别最后一个 Continuation 参数,从其泛型中还原响应类型,再用 suspendCancellableCoroutine 把 OkHttp enqueue 的回调、异常与取消信号桥接回调用方协程。
本文验证基线
- 字节码:Kotlin
2.2.10、JVM target17,使用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 级别的"魔法生命周期"。真正的生命周期拥有者是调用方的 CoroutineScope 和 Job。例如在 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);
有两个关键变化:
- 参数列表末尾追加了一个
Continuation<? super User>。 - 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等请求参数组装,因此对应的ParameterHandler是null。
真正构建 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(): Unit走awaitUnit的特殊处理。
还有一个看似绕弯的设计:源码会临时构造 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 回调到达后再调用 resume 或 resumeWithException,状态机从原挂起点之后继续。
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 代理限制。VirtualMachineError、ThreadDeath、LinkageError 等致命错误不会被吞掉。
六、线程到底在哪里切换
完整理解 Retrofit 协程支持,需要把三个概念分开:
| 层级 | 负责什么 | 不负责什么 |
|---|---|---|
suspend/状态机 |
保存执行位置,允许稍后恢复 | 不自动创建线程,不保证后台执行 |
OkHttp Dispatcher |
调度异步 HTTP 工作,维护并发请求队列 | 不拥有业务协程生命周期 |
协程 CoroutineDispatcher |
决定 Continuation 恢复后在哪个线程执行 | 不替 Retrofit 执行网络协议栈 |
典型情况下:
- 主线程中的
viewModelScope.launch调用挂起接口。 - Retrofit 调用 OkHttp
enqueue(),函数挂起,主线程不被网络等待占用。 - OkHttp 在自己的工作线程执行网络 I/O,并在响应路径中完成 Body 转换。
continuation.resume(...)提交恢复任务。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,不知道页面是否可见。使用 viewModelScope、lifecycleScope 或应用级作用域,是业务架构的所有权决策。
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子类。 label、result、L$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 入口打断点
按链路依次观察:
Retrofit.create()中的InvocationHandler.invoke()。Retrofit.loadServiceMethod()是否命中缓存。RequestFactory.Builder.parseParameter()是否将isKotlinSuspendFunction设为true。HttpServiceMethod.parseAnnotations()还原出的responseType。SuspendForBody.adapt()或SuspendForResponse.adapt()。KotlinExtensions.await()中的enqueue()、resume()与取消回调。
如果断点看到的方法参数是 (@Path id, Continuation),说明 Kotlin 降级阶段正常;如果响应类型错误,应继续检查 Continuation 的泛型签名、R8 规则、Converter 和自定义 CallAdapter。
十一、与 Call<T>、suspend T、Flow<T> 怎么选
| 形式 | 结果次数 | 启动方式 | 取消所有者 | 推荐用途 |
|---|---|---|---|---|
Call<T> |
一次 | execute 或 enqueue |
调用者显式 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 ...: T或Response<T>,不要返回Call<T>。 - 由
viewModelScope、lifecycleScope或明确的应用作用域拥有请求生命周期。 - 不吞掉
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 数据流上的不同阶段。
参考资料
- Kotlin Specification:Asynchronous programming with coroutines
- Kotlin API:Continuation
- kotlinx.coroutines API:suspendCancellableCoroutine
- Retrofit 3.0.0:Retrofit.java
- Retrofit 3.0.0:RequestFactory.java
- Retrofit 3.0.0:HttpServiceMethod.java
- Retrofit 3.0.0:KotlinExtensions.kt
- Retrofit CHANGELOG:2.6.0 原生支持 suspend
- Retrofit 3.0.0:KotlinSuspendTest.kt