OkHttp3+Retrofit2+RxJava3网络框架重构实战:从选型到落地

发布时间:2026/9/19 14:14:10
OkHttp3+Retrofit2+RxJava3网络框架重构实战:从选型到落地 最近在整理公司几个项目的网络层代码发现很多同学把OkHttp3、Retrofit2、RxJava3挂在嘴边但真到要自己从零搭一套网络框架时还是会踩不少坑。尤其这三年Retrofit2和OkHttp3的大版本迭代虽然不激进但RxJava3的切换把之前一堆老写法都废掉了。趁这次重构我把整个网络请求层全部重写了一遍把从选型到落地、从封装到不复原的完整思路整理出来这篇博文会讲的比较细读完完全可以照着在自己的项目里复刻一套。为什么选“OkHttp3Retrofit2RxJava3”这三件套而不是直接用Ktor或者其它新兴库核心原因有三方面一是团队现有技术栈的迁移成本最低大家都会二是OkHttp3的拦截器机制在这个生态里无人能敌做日志、缓存、鉴权都非常顺手三是Retrofit2把接口定义和HTTP协议绑定的非常优雅配合RxJava3的响应式编程业务层代码可以做到极简。这个组合不是最潮的但一定是最稳定、最可控的适合大多数商业项目。这套框架解决的核心痛点是业务代码里到处都是重复的线程切换、参数拼接、错误弹窗、Loading管理。把这些脏活累活全部下沉到框架层业务侧只需要声明接口、发起订阅、接收结果其它事情一概不管。1. 方案选型与整体设计思路1.1 网络库选型的底层考量聊选型之前先想清楚一个前提你的项目到底需要什么样的网络框架。“需要”三个字很关键因为很多项目并不是真的需要一套框架只是想找一个能发请求的库。如果你的团队只有两三个人、接口不超过几十个、不需要统一处理Token刷新和公共参数那就直接用Retrofit2裸写别封装。封装是有代价的每一层抽象都意味着排障成本的增加。但如果你面临的是下面的场景那就必须上框架接口上百个且存在大量重复的Header、公共参数改一处要动几十个接口有统一的BaseUrl切换需求比如正式环境和测试环境切换需要统一的Token失效处理刷新Token后自动重放失败请求业务侧代码大量出现重复的Loading状态控制、错误Toast接口返回有统一的代码和消息结构需要转成统一的业务状态我这次重构的目标就是在不引入过多黑魔法的情况下把这几个问题全部解决。OkHttp3作为HTTP层负责真正的网络通信、连接池管理、拦截器机制Retrofit2作为API层负责把Java接口方法映射成HTTP请求RxJava3作为调度层负责线程切换和响应式订阅把网络请求的异步过程变成流式操作。1.2 三者的职责边界与数据流有些同学会把三者的关系搞混总觉得Retrofit2内部已经做了异步处理为什么还要加RxJava3这里必须理清楚。OkHttp3本身支持同步Call和异步Call两种模式它的异步是基于Callback回调。Retrofit2默认也支持Call回调方式你可以直接传一个Callback进去这其实就是最原始的Retrofit用法。但这样写出来的代码嵌套几层回调之后可读性会急剧下降。RxJava3的介入不是替代OkHttp3的异步机制而是在它之上再包一层把回调变成流让业务层可以用链式调用的方式操作异步数据。整个请求链路是这样的业务层调用Retrofit接口方法得到一个Observable对象Retrofit2的RxJava3CallAdapterFactory负责把Call适配成ObservableObservable在IO线程执行真正的HTTP请求底层走OkHttp3请求结果通过RxJava3的map操作符转成业务模型通过subscribeOn和observeOn完成线程切换UI线程接收最终结果这样做的好处非常明显线程切换代码在框架层完成业务层不需要也不应该关心当前代码跑在哪个线程。这是一种强制性的纪律反而减少了出错的概率。1.3 为什么不用ViewModel协程替代现在用Kotlin的项目协程Flow其实也是很好的方案而且Google官方更推荐协程。但如果你维护的是老项目或者团队里还有人不太熟悉协程的挂起和调度原理Retrofit2RxJava3这套方案仍然有它的独特优势。RxJava3相比协程来说操作符更丰富处理复杂的数据变换和组合时更顺手。比如多个请求并发合并、错误重试、限流、去抖这些都是RxJava3的看家本领用协程写需要更多手写逻辑。还有一个现实问题很多老项目里的业务代码都是基于RxJava写的切换到协程意味着所有接口调用全部要改一遍这个迁移成本实在太高。所以我的建议是新项目不妨试试协程但老项目或者团队习惯偏向RxJava的项目直接用三件套是最理性的选择。别动不动想推翻重来能用最低成本把问题解决才是成熟工程师的选择。2. 网络框架分层设计与基础封装2.1 整体分层结构一套好的网络框架从上到下应该是清晰分层的。我这次搭的分层结构如下- 接口定义层API Interface - 仓库层Repository - 统一响应处理层Response Wrapper - 网络核心层Retrofit OkHttp3 - 日志与监控层Interceptor每一层只负责自己的事情下层不感知上层。接口定义层只写接口方法和注解不关心数据怎么处理仓库层负责数据转换和缓存逻辑网络核心层负责请求的发起和重试日志监控层只做横切关注点的处理。2.2 Retrofit实例构建的细节构建Retrofit实例是整套框架的起点看似简单但细节不少。先看代码object RetrofitClient { private const val BASE_URL https://api.example.com/ private const val TIME_OUT_SECONDS 15L private val okHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .connectTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS) .readTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS) .writeTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .addInterceptor(HttpHeaderInterceptor()) .addInterceptor(HttpLoggingInterceptor()) .addInterceptor(TokenAuthenticatorInterceptor()) .addInterceptor(CacheInterceptor()) .cache(cache) .build() } private val retrofit: Retrofit by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) .build() } fun T create(service: ClassT): T { return retrofit.create(service) } }注意几个点。超时时间为什么要拆成三个分别设置因为连接超时、读取超时、写入超时在实际网络环境中的表现差异很大有时候弱网环境下连接很快但读取很慢有时候上传大文件时写入超时这三个参数分开设置才能在出现问题的时候快速定位。retryOnConnectionFailure这里有个坑官方默认是true也就是连接失败会重试一次但对于POST请求这个重试可能会造成重复提交。如果业务场景不能接受重复提交这个参数建议显式设置为false让上层用RxJava3的retry操作符精确控制重试时机和次数。2.3 接口定义的最佳实践接口定义这里踩过最多的坑是泛型滥用和RequestMapping混乱。我推荐的做法是定义一个统一的泛型包装类这样泛型擦除的问题也能规避data class ApiResponseT( val code: Int, val message: String, val data: T? ) interface UserApi { POST(user/login) fun login(Body request: LoginRequest): ObservableApiResponseUserInfo GET(user/profile) fun getProfile(Query(userId) userId: String): ObservableApiResponseUserProfile POST(user/logout) fun logout(Header(Authorization) token: String): ObservableApiResponseUnit }ApiResponse这个包装类是整个框架的契约基础。很多接口不是统一的返回结构这是一个非常头疼的业务现实。我的处理方式是如果公司后端不统一返回格式那就分两套接口定义一套返回ApiResponse包装类型另一套直接返回原始类型在仓库层做适配。千万别试图用一个包装类强行套所有接口泛型擦除和类型转换会让你焦头烂额。3. OkHttp3拦截器层的核心细节3.1 请求头注入拦截器的正确写法请求头注入是最基础的需求公共参数、设备信息、版本号都需要在请求发出前自动加到Header里。很多人写Header拦截器时都会犯一个错误直接用request.newBuilder().addHeader()这样会导致同一个Header出现多个值。class HttpHeaderInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest chain.request() val requestBuilder originalRequest.newBuilder() .header(Content-Type, application/json; charsetUTF-8) .header(Accept, application/json) .header(App-Version, BuildConfig.VERSION_NAME) .header(Device-Id, DeviceUtils.getDeviceId()) .addHeader(Accept-Language, LocaleUtils.getCurrentLanguage()) .method(originalRequest.method, originalRequest.body) return chain.proceed(requestBuilder.build()) } }这里用header()不是addHeader()两者的区别在于header()会覆盖同名的所有旧值addHeader()是追加。对于Content-Type、Accept这种不能重复的Header必须用header()但对于某些允许重复的Header字段才用addHeader()。动手写之前先想清楚这个Header允许不允许重复这决定了你用哪个方法。3.2 日志拦截器与进阶优化OkHttp3官方提供了HttpLoggingInterceptor来做日志这是最基础的做法。但实际项目中官方这个日志有几个痛点一是在Release包没法自动关闭二是日志内容在Logcat里太长会被截断三是数据量太大影响性能。我的做法是包装一层class HttpLoggingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { if (!BuildConfig.DEBUG) return chain.proceed(chain.request()) val request chain.request() logD(-- ${request.method} ${request.url}) val response chain.proceed(request) val bodyString response.peekBody(1024 * 1024).string() logD(-- ${response.code} ${response.request.url}) if (bodyString.isNotEmpty()) { logJson(bodyString) } return response } }核心技巧是用peekBody()而不是body()。body()是流式读取读取之后Response就不能再用了会直接导致业务层解析失败。peekBody()会拷贝一小段内容读取之后不影响后续正常使用。这里设置1MB的上限因为一般接口响应不会超过这个值超过1MB的说明你的接口设计有问题或者压根不该走日志。日志封装完要考虑一个问题线上问题排查怎么办我的做法是Release包里保留一个可配置的日志开关通过本地调试面板远程打开日志写到文件而不是Logcat当天文件超过一定大小自动滚动。这样既保证了线上性能又能在用户反馈问题时快速抓日志。3.3 Token失效自动刷新与请求重放这个功能是拦截器层最复杂的部分也是最能体现框架价值的地方。核心逻辑是请求发出后如果返回401说明Token失效这时拦截器应该暂停当前请求去刷新Token刷新成功后再重放当前请求刷新失败则通知上层退出登录。class TokenAuthenticatorInterceptor( private val tokenManager: TokenManager, private val apiService: AuthApi ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest chain.request() val response chain.proceed(originalRequest) if (response.code 401 !originalRequest.url.toString().contains(auth/refresh)) { synchronized(TokenLock) { val hasRefreshed refreshToken() if (hasRefreshed) { return chain.proceed(newRequestWithNewToken(originalRequest)) } } } return response } private fun refreshToken(): Boolean { return try { val refreshToken tokenManager.getRefreshToken() val newToken apiService.refreshToken(refreshToken).execute().body() if (newToken ! null) { tokenManager.saveTokens(newToken.accessToken, newToken.refreshToken) true } else { tokenManager.clear() false } } catch (e: Exception) { false } } }这里最关键的坑是并发问题。假设在同一时间有5个请求都返回401如果不加锁5个请求会同时去刷新Token刷新5次返回结果还是最后一次生效前四次白白浪费。所以必须用synchronized或者更高级的锁机制保证同一时间只有一个线程在刷新Token。刷新Token时用的execute()方法是同步请求这在拦截器里是故意的。因为拦截器方法本身就是同步执行的如果你在拦截器里去用异步请求线程模型会混乱导致回调迟迟不执行。另外要注意刷新Token的接口本身不能走这个拦截器否则会死循环。我用url.contains(auth/refresh)来判断但更严谨的做法是给这个API加一个专属的Retrofit实例不走这个拦截器。3.4 合理的缓存策略设计OkHttp3的缓存策略需要服务器配合返回Cache-Control头。但很多时候后端同学根本配不齐缓存头这时可以从客户端指定强制缓存时间。class CacheInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request chain.request() val originalResponse chain.proceed(request) val cacheControl: String when (NetworkUtils.isNetworkAvailable()) { true - public, max-age60 false - public, only-if-cached, max-stale604800 } return originalResponse.newBuilder() .header(Cache-Control, cacheControl) .removeHeader(Pragma) .build() } }这个逻辑很直白有网的时候允许缓存60秒60秒内重复请求直接走缓存不发出真实网络请求没网的时候允许用7天内的缓存数据。这种策略对列表页、首页这种变化不频繁的接口太友好了体验上给人的感觉就是“秒开”。但要注意有些接口是不能缓存的比如用户隐私数据、余额信息。这种接口怎么办我采取的方式是在API接口上做一个穿透标识比如给接口请求URL的前面不加缓存拦截器单独放行。代码层面可以通过判断请求的Header来做给特定接口添加Tag在拦截器里识别Tag后跳过缓存逻辑。4. RxJava3线程调度与生命周期绑定的实操4.1 线程调度的标准范式RxJava2切换到RxJava3之后绝大部分API是兼容的但有一件事必须注意RxJava3的包名从io.reactivex变成了io.reactivex.rxjava3Maven坐标也不一样了。如果你在网上搜索资料的时候看到说RxJava2的用法代码大概率还能跑但要手动改import。线程调度的标准写法是apiService.login(LoginRequest(username, password)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ response - // 主线程接收结果 handleLoginSuccess(response) }, { throwable - // 主线程处理异常 handleError(throwable) })这个写法背后的机制值得理解一下subscribeOn指定的是Observable开始发送事件时执行的线程observeOn指定的是下游观察者接收事件的线程。subscribeOn和observeOn可以出现多次但subscribeOn只有第一次有效而observeOn每一次都会生效。这是RxJava的经典陷阱如果在复杂的链式操作里没搞明白这点很容易出现线程切换和预期不符的问题。4.2 生命周期绑定解决内存泄漏用RxJava做网络请求最容易踩的坑就是内存泄漏。Activity销毁了网络请求还没回来回调已经引用着Activity的上下文这就导致Activity无法被回收。解决办法是使用AutoDispose或者RxLifecycle。我这次用的是AutoDispose因为它的侵入性最小不需要让Activity继承特定的基类。apiService.getUserProfile(userId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .as(AutoDispose.autoDisposable(AndroidEventListeners.provider(this))) .subscribe({ response - // 安全接收结果 }, { throwable - // 安全处理异常 })AutoDispose的原理是在RxJava的事件流中间插入一个处理器Disposable它会监听Activity或Fragment的生命周期当onDestroy触发时会自动调用dispose()方法切断事件流。这样即使网络请求最终返回了回调也不会被执行的不会访问已销毁的View和上下文。注意Fragment里用this的时候要确保this确实实现了LifecycleOwner接口。AndroidX的Fragment和Activity都实现了这个接口但如果你在自定义View或者其他非LifecycleOwner里使用AutoDispose需要自己传入一个LifecycleProvider。4.3 链式组合操作符的场景实战RxJava3最爽的地方在于对多个请求做组合调度。这里分享三个我在实际项目中高频使用的场景。场景一两个接口完全并行都返回之后合并结果。这种适合首页这种需要同时展示用户信息和配置信息的场景两个接口没有依赖关系可以并发请求Observable.zip( apiService.getUserInfo(), apiService.getAppConfig(), BiFunction { userInfo: UserInfo, appConfig: AppConfig - HomeData(userInfo, appConfig) } ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ homeData - // 同时拿到两个数据 }, { error - // 处理异常 })zip操作符会等待两个Observable都发射完毕再合并如果其中一个接口特别慢整个合并会等那个慢的。所以在业务层面要评估清楚两个接口是否真的需要同时展示还是一个可以先展示一部分内容。场景二第二个接口依赖第一个接口的返回值。比如先拿用户Token再用Token去换用户详情apiService.getToken() .flatMap { token - apiService.getUserDetail(token) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ userDetail - // 拿到用户详情 }, { error - // 处理异常 })flatMap是你在这里的老朋友它把上游的每个事件转换成新的Observable然后合并这些Observable的事件最后发射到下游。这里要注意flatMap是乱序的如果你关心顺序要改用concatMap。场景三请求失败自动重试。网络抖动导致一次失败是很常见的事情无脑重试3次并不优雅更合理的做法是加上退避策略apiService.getData() .retryWhen { errors - errors.zipWith(Observable.range(1, 3)) { error, retryCount - if (retryCount 3) { throw error } retryCount }.flatMap { retryCount - Observable.timer((retryCount * 2).toLong(), TimeUnit.SECONDS) } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ data - // 成功 }, { error - // 重试3次后还是失败 })这里的逻辑是延迟重试第1次等2秒第2次等4秒第3次等6秒总共最多重试3次。这种退避策略比毫不停顿的立刻重试成功率要高得多也减轻了对服务端的压力。5. 统一数据解析与错误处理的全流程5.1 多层级的数据解析器设计网上的框架教程大多只展示GsonConverterFactory.create()一行代码但实际业务需求远没这么简单。真实项目中接口返回未必是规范的JSON可能有的字段是下划线风格有的是驼峰风格还可能是字符串里包着一个JSON。我这次做了两层解析策略。外层用Gson响应body的原始类型内层用自定义的TypeAdapter处理复杂字段class ApiResponseAdapterT(private val type: Type) : JsonDeserializerApiResponseT { override fun deserialize( json: JsonElement, typeOfT: Type, context: JsonDeserializationContext ): ApiResponseT { val jsonObject json.asJsonObject val code jsonObject.get(code).asInt val message jsonObject.get(message).asString val dataElement jsonObject.get(data) val data: T? if (dataElement ! null !dataElement.isJsonNull) { context.deserialize(dataElement, type) } else { null } return ApiResponse(code, message, data) } } class ApiResponseTypeAdapterFactory : TypeAdapterFactory { override fun T create(gson: Gson, type: TypeTokenT): TypeAdapterT? { if (type.rawType ! ApiResponse::class.java) return null val parameterizedType type.type as ParameterizedType val argumentType parameterizedType.actualTypeArguments[0] val adapter gson.getAdapter(TypeToken.get(argumentType)) return ApiResponseTypeAdapter(adapter) as TypeAdapterT } }GsonConverterFactory里传入这个自定义的TypeAdapterFactory这样Retrofit在反序列化ApiResponse时就会走我们的逻辑。这个设计的核心优势是data字段的解析可以完全复用Gson的泛型机制而code、message的处理逻辑完全收敛在框架层。下划线转驼峰的问题最简单的方案是反序列化时设置FieldNamingPolicyval gson GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapterFactory(ApiResponseTypeAdapterFactory()) .create()但这里要谨慎全局设置下划线转驼峰后如果你有的接口本来就返回驼峰就会解析不出来。我见过很多项目在这个问题上翻车所以建议还是要后端统一规范规范不了就interface里给每个字段都加SerializedName明确标注别指望靠一个全局策略解决问题。5.2 错误码统一封装与业务异常网络异常和业务异常应该明确区分。网络异常指连接超时、DNS解析失败这种底层问题业务异常指接口返回了code50001表示用户不存在。这两类异常的捕获和处理逻辑完全不一样。我的框架里定义了一个基类异常和一个业务异常class NetworkException( val errorCode: Int, val errorMessage: String, cause: Throwable? null ) : Exception(errorMessage, cause) class BusinessException( val businessCode: Int, override val message: String ) : Exception()然后在请求结果回到业务层之前统一做转换fun T ObservableApiResponseT.handleResult(): ObservableT { return this.map { response - if (response.code 200) { response.data } else { throw BusinessException(response.code, response.message) } } }这样业务层订阅时onNext只接收成功的业务数据onError里只需要做一次判断如果是BusinessException就处理业务错误弹Toast、跳登录如果是NetworkException就统一提示网络错误。业务层不再需要每个接口都写一遍code判断这是提升效率最明显的一点。错误码的映射也是可以做的。比如401应该跳到登录页403应该提示没有权限502应该提示服务器开小差了。这些映射逻辑可以塞进一个ErrorMapper类里统一处理。5.3 Loading状态管理的下沉业务层最烦的工作之一就是每个网络请求都要手动控制Loading的显示和隐藏。我见过很多项目在Activity里到处写着showLoading()和dismissLoading()一旦忘记隐藏就会引发崩溃。这次我把Loading管理也做进了框架。核心思路是在事件流发送开始时通知Loading展示事件流终止时通知Loading消失fun T ObservableT.bindLoading(loadingView: LoadingView): ObservableT { return this.doOnSubscribe { loadingView.showLoading() }.doOnTerminate { loadingView.hideLoading() } } fun T ObservableT.bindLoading( loadingView: LoadingView, showDelayMillis: Long 300 ): ObservableT { var showTask: Disposable? null return this.doOnSubscribe { showTask Observable.timer(showDelayMillis, TimeUnit.MILLISECONDS) .subscribe { loadingView.showLoading() } }.doOnTerminate { showTask?.dispose() loadingView.hideLoading() } }doOnSubscribe在订阅时执行doOnTerminate在事件流结束时执行无论成功还是失败都会执行。这里做了一个优化延迟300毫秒才显示Loading这样如果接口本身响应很快比如200毫秒用户根本感知不到Loading的存在避免了界面闪烁。这个细节对用户体验的提升是很实在的。6. 常见问题与排查技巧实录6.1 构建期问题如何从RxJava2迁移到RxJava3如果你和我一样是从老项目升级过来的迁移过程会比想象中麻烦。RxJava3不仅仅是换个包名这么简单有一些方法签名变了也有一些内置类被移除了。我整理了几个最常见的迁移失败场景import语句全部从io.reactivex改成io.reactivex.rxjava3Observable.empty()的行为变了需要手动指定类型否则编译不通过Maybe类型的处理方式不同subscribe成功回调的入参发生了变化Flowable新增了更多的背压策略一个偷懒但有效的办法是全局搜索“io.reactivex.”并全局替换成“io.reactivex.rxjava3.”但替换完之后不要以为就完事了编译一次把报错的地方逐个击破。实测下来项目里90%的代码只需要换import就能跑通剩下10%的兼容性问题集中在错误处理函数签名和自定义操作符上。6.2 运行时问题Gson解析LocalDate失败的坑Retrofit2配合Gson时如果你用的是Java 8的LocalDate类型Gson默认是解析不了的会直接报JsonSyntaxException。这是我踩得最深的坑之一。解决方案是注册一个LocalDateTypeAdapterclass LocalDateTypeAdapter : JsonDeserializerLocalDate, JsonSerializerLocalDate { override fun deserialize( json: JsonElement, typeOfT: Type, context: JsonDeserializationContext ): LocalDate { return LocalDate.parse(json.asString, DateTimeFormatter.ISO_LOCAL_DATE) } override fun serialize( src: LocalDate, typeOfSrc: Type, context: JsonSerializationContext ): JsonElement { return JsonPrimitive(src.format(DateTimeFormatter.ISO_LOCAL_DATE)) } } val gson GsonBuilder() .registerTypeAdapter(LocalDate::class.java, LocalDateTypeAdapter()) .create()更全面的做法是把LocalDateTime和OffsetDateTime也一并注册适配器否则早晚会在某个接口上踩雷。这个坑特别隐蔽因为本地测试时明明没问题一到线上某个接口返回了一个时间字段整个解析就挂掉了而且报错信息还不好定位。6.3 排查技巧怎么快速定位网络请求到底卡在哪一步网络请求慢或者不返回是最难排查的问题。排查的第一步是确认请求真发出去了。用日志拦截器看请求头和响应码如果日志里没有任何输出说明请求压根还没走到网络层问题出在调用方如果有请求日志但没有响应日志说明响应真的很慢或者被服务端挂起。第二步是看线程池。RxJava的Schedulers.io()默认使用无界线程池正常情况下没问题但如果你在代码里大量使用blockingXXX或者把耗时操作误放到IO调度器里线程会被占满。在日志里打印一下线程名和数量这是判断线程池健康程度的直接手段。第三步是抓Chuck或者自己写一个网络面板。我的做法是在Debug模式下给OkHttp3加一个自定义的EventListener把每次请求的DNS解析时间、连接时间、TLS握手时间、发送时间、等待时间、接收时间全部统计出来。可以看到一个请求到底慢在哪个阶段DNS慢就优化DNS等待慢就排查服务端逻辑。这一步信息量非常大强烈建议有条件的话都做上。6.4 常见问题速查表症状可能原因解决方案Retrofit接口无法解析接口没有加注解或注解里缺少HTTP方法检查GET、POST等注解是否完整参数拼接错误Query和Path用错位置Query用于拼接参数Path用于替换路径发送请求后无任何日志拦截器顺序不对或日志开关被关闭确认日志拦截器在Interceptor链中且BuildConfig.DEBUG为true回调不执行未添加RxJava3CallAdapterFactory检查Builder里是否调用addCallAdapterFactory请求成功但解析为空服务器返回非Json格式或Gson字段名不匹配打开日志打印原始body逐个对比字段名内存泄漏网络订阅未解除使用AutoDispose或手动管理DisposableAndroid 9以上无法访问HTTP系统默认禁止明文HTTP配置usesCleartextTraffic或networkSecurityConfig证书校验失败HTTPS证书不合法或日期过期检查证书链和有效期正式包不要关闭验证6.5 几个独家避坑技巧最后再分享几个我重写这套框架时踩出的心得这些在官方文档里绝对找不到。URL拼接上统一走HttpUrl。我在代码里看到过各种千奇百怪的URL拼接方式有的直接字符串加号拼接有的用String.format一旦参数为空或者带特殊字符就直接崩掉。正确做法是val url HttpUrl.Builder() .scheme(https) .host(api.example.com) .addPathSegment(user) .addQueryParameter(id, userId) .build() .toString()这样做URL编码全部由HttpUrl处理永远不会出现中文乱码和特殊字符错误。不要使用Call.enqueue。Retrofit2默认支持Call方式但如果你已经用了RxJava3就不要在代码里混着用Call.enqueue。两种异步模式混用会带来巨大的心智负担。我这次统一要求代码里禁止使用Call类型全部走Observable。Review代码时看到Call直接打回。文件上传下载单独处理。大文件上传不能走普通的JSON接口要单独封装一个上传接口用MultipartBody加自定义进度回调的RequestBody。下载则要单独处理流式读取不能把整个文件读进内存。这些场景如果塞进通用框架里会让框架变得臃肿建议单独开服务类处理框架只负责通用请求。统一在框架层打印完整日志。日志不是Debug专属品线上问题排查时日志可能是唯一的线索。我的做法是框架层预留一个LogSink接口Debug时输出到LogcatRelease时可以配置输出到文件同时支持远程开关。这个不会增加多少开发成本但紧急故障时能救你一命。经过这一轮重构最直观的收益是新增一个接口的代码量大幅下降。定义好接口、仓库里一行调用、Activity里订阅三步就完事。对团队来说因为所有网络请求都收敛到统一的链路上代码审查的负担也轻了很多错误的可能性被限制在了框架内部。这套三件套方案也许不是最前沿的但如果你想要一个稳定、可控、团队容易上手的网络层它仍然是目前综合性价比最高的选择之一。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询