You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Retrofit2与OkHttp3异常触发式缓存回退实现及问题排查

全场景网络异常下的本地缓存兜底实现方案

核心需求对齐

  • 每次接口拉取成功时强制更新本地缓存,适配每5秒轮询拉取的场景
  • 任意一次拉取失败时,直接返回最近一次的有效缓存;拉取成功则返回线上最新数据
  • 缓存有效期支持数天到数周,仅在后续拉取成功时覆盖更新原有缓存
  • 无可用缓存且首次拉取失败时,才向用户展示错误提示

现有实现的核心问题定位

根因拆解

现有实现中,「Wi-Fi已连接但无互联网通路」场景下回退逻辑不执行的核心原因有三点:

  • 拦截器顺序逻辑错位:配置将onlineOfflineHandling作为应用层拦截器添加,同时把强制缓存逻辑写在网络拦截器中,当线上请求超时抛出异常时,异常抛出的位置会直接被OkHttp调用链捕获,不会进入catch分支的缓存回退逻辑——尤其是超时类异常(对应已联网但无数据通路的场景),异常会在网络拦截器阶段就抛出,根本不会触发应用层拦截器的catch块。
  • 服务端响应头的缓存规则未被强制覆盖:绝大多数第三方API(比如YouTube API)返回的响应头会携带Cache-Control: no-cache/no-store,如果不强制覆盖这类响应头的缓存规则,OkHttp根本不会把成功的响应写入本地缓存,自然在需要回退的时候读不到缓存数据。
  • 回退请求的容错缺失:构造带onlyIfCached的离线请求发起时,如果本地没有对应缓存,这个请求本身会抛出IOException,如果不做二次捕获,就会直接把异常抛给上层的RxJava订阅,导致用户收不到任何响应。

可直接复用的修正版实现

OkHttp客户端配置

拦截器顺序的优先级、缓存规则强制覆盖、异常二次捕获是实现全场景兜底的核心:

interface EndpointServices {
    companion object {
        // 强制覆盖服务端响应头,将所有成功响应写入本地缓存,支持最长30天的缓存有效期
        private fun forceCacheInterceptor(): Interceptor {
            return Interceptor { chain ->
                val request = chain.request()
                val originalResponse = chain.proceed(request)
                // 仅对成功响应做缓存写入,4xx/5xx错误不写入缓存,避免污染兜底数据
                if (originalResponse.isSuccessful) {
                    val cacheControl = CacheControl.Builder()
                        .maxAge(0, TimeUnit.SECONDS) // 强制每次发起请求时优先取线上最新数据
                        .maxStale(30, TimeUnit.DAYS) // 缓存最长保留30天,匹配数周有效周期的需求
                        .build()
                    originalResponse.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", cacheControl.toString())
                        .build()
                } else {
                    originalResponse
                }
            }
        }

        // 异常回退拦截器:必须放置在最外层应用拦截器位置,捕获所有请求阶段的异常
        private fun fallbackToCacheInterceptor(): Interceptor {
            return Interceptor { chain ->
                val request = chain.request()
                try {
                    // 正常请求流程:获取线上最新数据
                    chain.proceed(request)
                } catch (e: Exception) {
                    // 覆盖所有异常场景:超时、连接失败、API配额超限、网络无通路等
                    Log.w("INTERCEPT", "请求失败,触发缓存回退: ${e.message}")
                    val cacheRequest = request.newBuilder()
                        .cacheControl(
                            CacheControl.Builder()
                                .maxStale(30, TimeUnit.DAYS)
                                .onlyIfCached()
                                .build()
                        )
                        .build()
                    try {
                        // 读取本地缓存返回兜底数据
                        val cachedResponse = chain.proceed(cacheRequest)
                        Log.w("INTERCEPT", "成功读取缓存数据")
                        cachedResponse
                    } catch (cacheException: Exception) {
                        // 无可用缓存时,将原始异常抛给上层,走用户错误提示逻辑
                        Log.e("INTERCEPT", "无可用本地缓存")
                        throw e
                    }
                }
            }
        }

        fun create(baseUrl: String, context: Context): EndpointServices {
            // 150MB缓存容量,适配多资产的本地存储需求
            val cacheSize = 150 * 1024 * 1024L
            val cache = Cache(context.cacheDir, cacheSize)

            val loggingInterceptor = HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            }

            val httpClient = OkHttpClient.Builder()
                .cache(cache)
                // 拦截器顺序严格按照优先级配置:
                // 1. 最外层放置兜底拦截器,第一时间捕获所有请求异常
                .addInterceptor(fallbackToCacheInterceptor())
                .addInterceptor(loggingInterceptor)
                // 网络拦截器负责强制覆盖响应的缓存规则,确保成功响应能写入本地缓存
                .addNetworkInterceptor(forceCacheInterceptor())
                .callTimeout(10, TimeUnit.SECONDS)
                .connectTimeout(10, TimeUnit.SECONDS)
                .build()

            val retrofit = Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(httpClient)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(MoshiConverterFactory.create())
                .build()

            return retrofit.create(EndpointServices::class.java)
        }
    }

    @GET("search")
    fun getVideoItems(
        @Query("key") key: String,
        @Query("part") part: String,
        @Query("maxResults") maxResults: String,
        @Query("order") order: String,
        @Query("type") type: String,
        @Query("channelId") channelId: String
    ): Single<VideoItemModel>
}

上层调用逻辑优化

  • 全局复用Retrofit实例:之前每次发起请求都重新调用EndpointServices.create()的写法,会每次创建新的OkHttp和Cache实例,导致缓存完全不生效,需将服务实例做成全局单例,复用同一个Cache对象。
  • 统一响应渲染逻辑:不管是线上拉取的新数据还是回退的缓存数据,统一走UI渲染流程,不需要区分数据来源:
// 全局单例初始化,避免重复创建客户端实例
private val endpointService = EndpointServices.create(Config.BASE_URL, applicationContext)

// 5秒轮询逻辑
intervalDisposable = Observable.interval(0L, 5L, TimeUnit.SECONDS)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe {
        fetchVideoList()
    }

private fun fetchVideoList() {
    disposable = endpointService.getVideoItems(
        AppUtils.videoKey,
        "id,snippet",
        "20",
        "date",
        "video",
        channelId
    )
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(
            { result ->
                adapter.submitList(result.videoData)
                swipeRefreshLayout.isRefreshing = false
                logTxt.text = null
            },
            { error ->
                // 仅在无可用缓存、请求失败时展示错误提示
                swipeRefreshLayout.isRefreshing = false
                logTxt.text = getString(R.string.swipeToRefresh)
                Log.e("FETCH_ERROR", error.message, error)
            }
        )
}

修正后全场景验证效果

  • 移动网络/Wi-Fi完全关闭:请求异常触发,直接读取本地缓存返回兜底数据
  • Wi-Fi连接但无互联网通路:请求超时触发异常,拦截器捕获后读取本地缓存返回,回退逻辑正常执行
  • 网络通路正常:直接拉取线上最新数据,拉取成功后自动更新覆盖本地缓存
  • 首次打开无缓存且请求失败:直接抛异常给上层展示错误提示

内容的提问来源于stack exchange,提问作者Mihae Kheel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.26 07:15:01