Ktor 客户端替代 Retrofit,协程原生支持
Ktor 客户端替代 Retrofit,协程原生支持
Retrofit 从 2.6.0 开始支持 suspend 函数,这让很多 Android 开发者误以为网络层已经彻底拥抱了 Kotlin 协程。但如果你打开 Retrofit 的源码,跟到 HttpServiceMethod.parseAnnotations() 的实现,会发现所谓的协程支持其实是一层薄薄的语法糖:它底层仍然创建的是一个 OkHttp Call,通过 suspendCancellableCoroutine 把 Callback 的 onResponse / onFailure 回调桥接成了挂起函数的恢复点。这意味着线程调度、连接取消、异常链路都还是 OkHttp 的模型,协程在这里更像是一个外部包装器,而不是原生的执行语义。当项目规模变大,开始用 Kotlin Flow 处理复杂的数据流、依赖协程的取消传播做生命周期管理时,这种"半透明"的协程支持就会暴露出缝隙。我遇到过的情况是:在 ViewModel 的 viewModelScope 被取消时,Retrofit 的底层 Call 确实会触发 cancel(),但协程异常处理器捕获到的却是 CancellationException 和 IOException 混杂的栈,链路并不干净。
Ktor Client 是 JetBrains 维护的网络库,定位上与 Retrofit 不同,它不是一个注解驱动的接口生成器,而是一个基于 Kotlin 协程原生构建的 HTTP 客户端。GitHub 仓库位于 github.com/JetBrains/ktor,采用 Apache 2.0 协议,完全免费。它从设计之初就把 kotlinx.coroutines 的挂起、取消、异常作为一等公民,而不是后期嫁接。这篇文章想聊的,正是从 Retrofit 迁移到 Ktor Client 过程中那些代码层面上的真实差异,以及它到底解决了什么问题、又带来了哪些新的麻烦。
Retrofit 的 suspend 只是一层包装
要理解 Ktor 的替换价值,得先看清 Retrofit 的 suspend 到底做了什么。Retrofit 2.6.0 引入的 HttpServiceMethod.SuspendForResponse 和 SuspendForBody 在运行时通过动态代理生成接口实现,当方法被标记为 suspend 时,Retrofit 并不会改变其底层的网络执行模型。它仍然走 OkHttpCall.enqueue(),也就是异步双回调的老路,只是用 Kotlin 标准库里的 suspendCancellableCoroutine 把回调结果转发给了续体(Continuation)。这种设计的直接后果是,协程的取消需要经过一次桥接:协程的 Job.cancel() 会传递到 Continuation,然后触发 Call.cancel(),最后由 OkHttp 的 Dispatcher 去中断线程。这个链条是有效的,但它不是原生的。如果你在一个自定义的 CoroutineDispatcher 或者 LimitedParallelism 上下文里执行请求,Retrofit 对此一无所知,它的并发控制仍然发生在 OkHttp 的连接池和 Dispatcher 层面。
更具体地说,Retrofit 的 suspend 函数返回类型要么是 Response<T>,要么是 T。如果请求失败,抛出的异常永远是 IOException 或其子类 HttpException(当 HTTP 状态码 >= 400 时)。这在协程的异常处理语境下显得过于粗放了。协程的 SupervisorJob 和异常传播机制期待的是能够精确区分"可恢复错误"和"致命错误",但 Retrofit 把所有问题都塞进了这两个异常类型里,导致在实际项目中我们经常要写一层薄薄的 Repository 封装来做异常翻译。这不是无法克服的问题,但它说明 Retrofit 的协程支持是向后兼容的妥协产物,而不是重新设计。
Ktor 的请求管道完全运行在协程里
Ktor Client 的核心是一个 HttpClient,它本身不处理底层 IO,而是依赖 Engine(引擎)来做真正的 socket 通信。关键在于,无论你选择 OkHttp 引擎、CIO(Coroutine I/O)引擎还是 Android 引擎,Ktor 的请求管道(Pipeline)都是完全构建在挂起函数之上的。你写的 client.get { url("...") } 不是一个被包装的回调,而是一个从顶到底都支持挂起和恢复的调用链。
这种原生协程语义带来了几个代码层面可感知