Square 开源库全家桶:OkHttp、Retrofit、Moshi
Square 开源库全家桶:OkHttp、Retrofit、Moshi
Square 这家公司在国内开发者社区里存在感不算特别高,但只要你写过几年 Android,几乎不可能没用过他们的库。OkHttp 作为 Android 4.4 之后默认的 HTTP 客户端,Retrofit 几乎是 REST API 封装的事实标准,Moshi 虽然被 Gson 压着一头,但这些年在新项目里的采用率明显在涨。这三个库出自同一家公司,设计哲学一脉相承,但各自的演进路线和当下的使用成本,其实有不少值得细说的东西。
OkHttp:从"默认选项"到需要主动选择
Android 4.4 KitKat 开始,HttpURLConnection 的底层实现换成了 OkHttp,这是很多人第一次间接用到它。但真正把它当作独立库引入项目,通常是为了连接池、拦截器、HTTP/2 支持这些能力。
OkHttp 4.x 版本用 Kotlin 重写了一遍,API 表面看起来变化不大,内部实现有不少调整。最实际的影响是依赖体积:4.x 引入了 Kotlin 标准库依赖,如果你的项目本身没 Kotlin,这会凭空多出几百 KB。Google 在 Android 开发上全面押注 Kotlin 之后,这不算什么问题,但 2019 年前后确实有不少纯 Java 项目因为这个在升级时犹豫。
拦截器(Interceptor)是 OkHttp 最核心的设计。应用拦截器和网络拦截器的区别,官方文档说得清楚,但新手踩坑率依然很高。应用拦截器不会看到重试和重定向的过程,网络拦截器能看到完整的请求链条但拿不到最原始的 Request 对象。如果你要做签名验签,通常得用应用拦截器;如果要抓包看实际发出去的 HTTP 内容,得用网络拦截器。这个设计有它的道理,但"两个拦截器"的区分确实增加了理解成本。
连接池的配置是个容易被忽略的点。OkHttp 默认保持 5 个空闲连接,每个连接存活 5 分钟。对于高频请求的场景,比如埋点上报或者实时消息同步,这个默认值往往不够用。但调大之后又要面对一个问题:Android 后台限制。Android 8.0 之后后台应用的网络访问被严格限制,保持大量空闲连接反而可能触发系统清理,导致下次前台使用时需要重新握手,TLS 1.3 的 0-RTT 能缓解一些,但前提是对端支持。
HTTP/3 的支持在 OkHttp 5.0.0-alpha 系列里已经能看到,基于 cronet 的 QUIC 实现。但 alpha 版本带了多久了?从 2021 年到现在,正式版迟迟没发。我个人不会在生产环境用 alpha 的 OkHttp,毕竟网络库是基础设施,出问题的排查成本太高。Cronet 本身倒是 Google 在 Chrome 里验证过的,但 OkHttp 的封装层和 API 稳定性还没承诺。
OkHttp 的局限也很实在。它不支持自动重试的精细化配置,比如只对幂等请求重试、按错误码区分重试策略,这些得自己在拦截器里实现。WebSocket 支持有,但功能比较基础,没有自动重连、心跳间隔的动态调整,复杂场景下很多人会选择 scarlet 或者干脆上专门的 WebSocket 库。另外,OkHttp 的缓存控制基于 HTTP 语义,如果你想做应用级别的离线缓存策略,得自己搭一层,它不会帮你处理"没网时读本地、有网时优先网络"这种常见需求。
Retrofit:注解驱动的代价
Retrofit 的优雅是出了名的。定义一个接口,加几个注解,就能发起 HTTP 请求,配合 Kotlin 的协程或者 RxJava,异步代码写起来很干净。但这种优雅是有前提的:你的 API 设计得规范,而且变动不频繁。
Retrofit 2.6 开始支持 suspend 函数,这是 Kotlin 协程普及的关键节点。之前的 Call 适配器模式虽然也能用,但协程的写法明显更贴合 Kotlin 的语法习惯。不过这里有个细节:suspend 函数的返回值直接是数据类,错误处理得用 try-catch 或者封装 Result 类型。Retrofit 本身不会把 HTTP 错误码自动映射成异常,404 和 500 默认都会抛 HttpException,但业务层面的错误(比如服务端返回 200 OK 但 body 里是 {"code": 1001, "msg": "参数错误"})得自己写 Converter 或者在拦截器里处理。
Converter 的选择是 Retrofit 使用中的第一个大决策。Moshi、Gson、Jackson、Kotlinx.serialization 都能接,但行为差异不小。Gson 最老,对 Kotlin 的默认参数支持有问题,data class 的默认值在反序列化时会被忽略,除非用专门的 TypeAdapter。Kotlinx.serialization 最"原生",但要求数据类加 @Serializable 注解,而且它对泛型的处理在某些边缘场景下会出问题。Retrofit 本身不绑定 Converter,这个设计是对的,但意味着选型时要自己承担组合风险。
动态 URL 和 BaseUrl 的切换是另一个常见痛点。Retrofit 实例创建时 BaseUrl 就固定了,运行时切换得用 @Url 注解让方法参数传完整 URL,或者搞多个 Retrofit 实例。后者在大型项目里会导致连接池不共享,内存占用上升。Square 官方没有提供动态 BaseUrl 的优雅方案,社区里有 retrofit-url-manager 这类封装,但维护状态参差不齐。
Retrofit 的注解系统在复杂查询参数场景下会显得笨拙。比如一个搜索接口有十几个可选参数,用 @QueryMap 可以传 Map,但类型安全就没了;用 @Query 一个个写,接口定义冗长。POST 请求的 multipart 上传更麻烦,@PartMap 的键值类型限制、文件和文本混传时的 Content-Disposition 组装,这些细节文档不会细讲,得翻源码或者看 issue 里的讨论。
最让我个人不太满意的是 Retrofit 的测试支持。MockWebServer 是 Square 出的配套工具,能模拟 HTTP 响应,但写集成测试的成本不低。你要搭一个完整的请求链路,包括 Converter 和 CallAdapter 的行为。单元测试层面,Retrofit 接口是动态代理生成的,直接 mock 返回值会绕过 Converter,测不到真实序列化逻辑。很多人最后选择把 API 层做薄,业务逻辑下沉到 Repository,但这也说明 Retrofit 本身不是能随便替换的层,它的注解定义和项目结构耦合得比较深。
Moshi:Gson 的替代品,但不是平替
Moshi 的定位很清晰:Kotlin 优先的 JSON 库。Gson 是 Java 时代的产物,对 Kotlin 的 nullable、默认参数、泛型型变支持都有缺陷。Moshi 用 KotlinPoet 生成代码,避免了反射带来的性能和 ProGuard 风险,这是它的核心卖点。
Moshi 的代码生成通过 kapt 或者 KSP 实现。kapt 时代编译速度是硬伤,大型项目里 JSON 适配器的生成能明显拖慢构建。KSP 的支持在 1.13.0 版本加入,编译速度提升显著,但迁移成本取决于你项目里 kapt 的依赖深度。如果还在用 kapt 的 Dagger 或者其他处理器,KSP 的切换得整体规划,不能单独给 Moshi 切。
Moshi 的 Kotlin 支持确实比 Gson 好。非空类型会生成正确的 null 检查,默认值能保留,泛型也不会像 Gson 那样因为 TypeToken 的坑搞出运行时 ClassCastException。但"好"不等于"没有坑"。Moshi 的代码生成要求数据类有明确的构造参数,如果你用了自定义的 getter/setter、委托属性,或者某些复杂的继承结构,代码生成会失败或者行为不对。这种情况得 fallback 到反射适配器,但反射适配器又需要 keep 规则,在 R8 全量模式下可能被误删。
自定义 JsonAdapter 是 Moshi 的进阶用法。比如服务端返回的日期格式不标准,或者某些字段需要运行时加密解密。写 JsonAdapter 的门槛比 Gson 的 TypeAdapter 高一些,Moshi 的 API 设计更强调组合和委托,比如可以通过 JsonAdapter.nullSafe()、JsonAdapter.lenient() 来包装已有适配器。这种设计有函数式编程的味道,但文档示例不够丰富,很多时候得看 test case 学用法。
Moshi 和 Retrofit 的配合有个细节:Retrofit 的 MoshiConverterFactory 默认用的是 Moshi 实例的 newBuilder() 构建,如果你自定义了 Moshi 实例(比如加了自定义 Adapter),记得显式传进去。这个坑在新手期很常见,表现就是自定义 Adapter 没生效,调试半天发现 Retrofit 用了默认的 Moshi 实例。
Moshi 的局限还包括对 Java 的兼容。虽然它能处理 Java 类,但代码生成是 Kotlin 特化的,纯 Java 项目用 Moshi 反而不如 Gson 顺手。另外,Moshi 的流式解析 API(JsonReader/JsonWriter)比 Gson 的 JsonElement 树模型更底层,如果你需要动态解析 JSON 结构(比如字段名不确定、结构随版本变化),Moshi 写起来更繁琐。Gson 的 JsonObject 可以随便 get/set,Moshi 得自己维护状态机式的读取逻辑。
三个库的组合成本
Square 这三个库设计上是能无缝配合的,但实际项目里把它们串起来,隐形成本不少。
依赖版本的对齐是第一个问题。OkHttp 4.10.0 和 Retrofit 2.9.0 的传递依赖可能有冲突,比如 OkHttp 的 kotlin-stdlib 版本和项目里其他 Kotlin 库不一致。Gradle 的依赖解析通常能自动选最新版,但 Android Gradle Plugin 的旧版本在解析策略上有 bug,可能导致运行时 NoSuchMethodError。稳妥的做法是显式声明 bom 或者统一版本变量,但这也增加了维护负担。
线程模型的理解成本被低估了。Retrofit 的 Call 默认在 OkHttp 的线程池执行,回调通过 Executor 切到主线程。用协程时,suspend 函数的执行上下文取决于调用点,但内部网络请求还是在 OkHttp 的 Dispatcher 里。如果你同时用了 RxJava 的 CallAdapter,线程切换的逻辑又不一样。这些细节不会体现在业务代码里,但排查 ANR 或者线程泄漏时,得清楚请求到底在哪个线程、回调怎么切的。
错误处理的统一是架构层面的挑战。Retrofit 抛的异常类型有:IOException(网络层)、HttpException(HTTP 错误码)、JsonDataException(Moshi 解析失败)、自定义的业务异常。这些异常分散在不同层级,上层业务通常想要一个统一的 Result 类型。社区里有不少封装方案,比如用 Arrow 的 Either、Kotlin 的 Result、或者自己封的 sealed class。但 Retrofit 的 CallAdapter 扩展点设计得不够灵活,实现一个能覆盖所有异常类型的通用 Adapter,需要对 Retrofit 的内部机制有较深了解。
缓存策略的跨层协调也是个麻烦。OkHttp 的 Cache 控制 HTTP 语义层面的缓存,Moshi 负责序列化,Retrofit 管接口定义。如果你要做应用级缓存(比如按用户 ID 隔离、按业务场景设置不同过期时间),这三个库都不会直接帮你。通常的做法是在 Repository 层加一层缓存抽象,用 Room 或者内存缓存,但这也意味着 Retrofit 的响应不会自动走缓存,得自己设计缓存 key 和失效策略。
版本迁移的真实经历
说点具体的版本迁移经验。OkHttp 3.12.x 到 4.x 的升级,除了 Kotlin 依赖问题,还有一个 SSL 相关的变化:OkHttp 4.x 默认禁用了 TLS 1.0 和 1.1,只保留 1.2 和 1.3。如果你的服务端是老旧的配置,升级后可能直接连不上。这个变更在 changelog 里有,但不够醒目,很多人是线上报 SSLHandshakeException 才发现。
Retrofit 2.6 到 2.9 之间主要是 bugfix 和协程支持的完善,但 2.9 的 suspend 函数在特定场景下有内存泄漏问题:如果请求被取消,但 Converter 还在处理响应体,协程的 Job 取消不会立刻中断 IO 操作。这个问题在 2.9.0 的 issue #3537 里有讨论,后续版本修复了。如果你的项目卡在 2.9.0,建议至少升到 2.9.1。
Moshi 的 1.14.0 版本开始要求 Kotlin 1.7.0,KSP 的最低版本也相应提高。我们项目去年卡在 Kotlin 1.6.21 因为 Compose Compiler 的兼容问题,Moshi 就没法升到最新。这种依赖链的传递约束在 Kotlin 生态里很常见,不是 Square 的锅,但确实增加了技术决策的复杂度。
替代方案的考量
这三个库不是唯一选择,值得在选型时看看别的。
网络层,Ktor Client 是 Kotlin 官方出的,协程原生支持,多平台目标(Android、iOS、桌面、JS)比 OkHttp 清晰。但 Ktor 的引擎选择多,CIO 引擎纯 Kotlin 实现,性能不如基于 NIO 的 OkHttp;Java 引擎依赖 OkHttp 本身,那为什么不直接用 OkHttp?Ktor 的 API 设计更现代,插件机制比拦截器灵活,但社区成熟度和第三方库配套不如 OkHttp。如果你在做 Kotlin Multiplatform,Ktor 几乎是必选项;纯 Android 项目,OkHttp 的迁移收益不明显。
API 封装层,Ktor 的 HttpClient 支持声明式 API 定义,和 Retrofit 的接口注解类似,但类型安全更强,因为利用了 Kotlin 的 DSL 和类型推断。不过 Ktor 的 API 定义和 Retrofit 的接口定义迁移成本不低,大型项目里接口文件可能上百个,重写不现实。另外,Retrofit 的 CallAdapter 生态(RxJava、LiveData、Flow、Coroutine)非常成熟,Ktor 的对应封装还在追赶。
JSON 解析,Kotlinx.serialization 是 Kotlin 官方方案,编译器插件生成代码,不需要 kapt/KSP,理论上构建速度最优。但实际用下来,它对多态序列化的支持(sealed class 的 polymorphic 处理)配置繁琐,而且和某些 ProGuard/R8 规则有冲突。Moshi 的代码生成虽然多一步,但调试和自定义空间更大。Gson 在维护状态上确实不如前两者积极,但存量项目迁移的成本往往被低估——不只是改依赖,还有自定义 TypeAdapter、历史数据兼容性、测试用例的覆盖。
实际项目中的配置建议
如果今天开一个 Android 新项目,我的配置会是这样:
OkHttp 用 4.12.0(当前稳定版),显式配置连接池和超时,不要全用默认。拦截器分层:日志用网络拦截器配合 HttpLoggingInterceptor(注意 release 包要关掉或者脱敏);签名鉴权用应用拦截器;重试逻辑单独写一个,避免和日志、签名耦合。缓存根据业务场景开,不要无脑加 Cache,磁盘缓存的清理策略要考虑用户主动清除缓存的情况。
Retrofit 用 2.9.0 或更高,Converter 选 Moshi,CallAdapter 用 suspend 函数,不再引入 RxJava 或 LiveData 的适配器。接口定义尽量扁平,不要嵌套太多层的泛型,Moshi 的代码生成对复杂泛型有时会生成冗长的适配器类,增加包体积。BaseUrl 如果有多套环境,用 BuildConfig 区分多个 Retrofit 实例,但共享同一个 OkHttpClient 实例以复用连接池。
Moshi 用 KSP 代码生成,数据类加 @JsonClass(generateAdapter = true),非空字段不给默认值(除非业务语义需要),避免 null 和默认值混用导致的行为模糊。自定义 JsonAdapter 集中管理,不要散落在各个模块。R8 规则用 Moshi 官方提供的 proguard-rules,不要自己从头写。
这套组合在 2024 年的 Android 项目里算是稳妥选择,但"稳妥"不等于"最先进"。Square 这些库的更新节奏明显放缓,OkHttp 的 HTTP/3 还在 alpha,Retrofit 的最后一个正式版是 2022 年,Moshi 的更新也以维护为主。它们不会消失,但也不会带来惊喜了。
最后一点个人看法
Square 的开源库有个共同特点:设计精致,但文档和周边工具跟不上。OkHttp 的拦截器文档讲了概念,没讲常见组合模式;Retrofit 的 CallAdapter 扩展机制藏在源码里,没有官方示例;Moshi 的自定义 Adapter 写法得看 test case。这种"代码即文档"的风格对资深开发者友好,但提高了入门门槛。
相比之下,Jetpack 库的文档和 codelab 体系更完善,Google 在开发者体验上投入更多。但 Jetpack 的库往往更"重",Lifecycle、ViewModel、Room 这些和 Android 框架绑定深,不像 Square 的库能在 JVM 后端或者其他场景复用。Retrofit 配合 Spring Boot 做微服务客户端、OkHttp 在 CLI 工具里发 HTTP 请求,这些用法虽然小众,但说明 Square 的设计保持了足够的通用性。
选型时我会考虑团队的技术储备和项目的生命周期。短期项目、团队熟悉 Gson,没必要硬切 Moshi;长期维护、Kotlin 为主的新项目,Moshi 的类型安全收益值得投入迁移成本。Retrofit 的接口定义一旦铺开,换掉的可能性极低,所以初期设计时多花时间在错误处理和测试策略上,比后期重构划算得多。
这些库用了这么多年,bug 踩过、issue 翻过、源码也读过,整体还是信任的。只是开源项目的维护周期和公司的商业重心总会偏移,Square 现在更出名的是 Cash App 和加密货币业务,Android 基础设施的更新优先级自然下降。作为使用者,理解这个背景,对版本选择和长期规划有帮助,但不必过度解读。代码还在,license 还是 Apache 2.0,够用了。