改进2:缓存响应到期后缓存不起作用

时间:2016-08-01 03:32:32

标签: android caching retrofit okhttp okhttp3

在我的Android应用程序中,我使用带有捆绑的okhttp的改装2。 我使用以下代码设置缓存

OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();

File httpCacheDirectory = new File(MyApplication.getInstance().getCacheDir(), "responses");
Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
httpBuilder.cache(cache);


OkHttpClient httpClient = httpBuilder.build();

Retrofit.Builder builder = new Retrofit.Builder().
        baseUrl(ApplicationConstants.BASE_API_URL).
        client(httpClient).
        addConverterFactory(GsonConverterFactory.create(gson));

正在从服务器端的响应上设置缓存标头。它可以很好地缓存文件并从缓存中显示它们,直到缓存的文件过期。

问题是当缓存过期时,它不能再被缓存。这不再缓存或替换旧的缓存文件。我认为它应该自动清理旧的无效缓存文件并替换为新的响应并缓存它。

如何清除无效响应并缓存新的有效响应。

我现在已经尝试了近两天,没有解决方案。事实上,对我而言,我似乎按照文档做了一切。还有别的东西可能是错的。

以下是来自okhttp的回复日志

D/OkHttp: Connection: keep-alive
D/OkHttp: Content-Type: application/json; charset=utf-8
D/OkHttp: Vary: Accept-Encoding
D/OkHttp: Transfer-Encoding: chunked
D/OkHttp: Server: Cowboy
D/OkHttp: X-Frame-Options: SAMEORIGIN
D/OkHttp: X-Xss-Protection: 1; mode=block
D/OkHttp: X-Content-Type-Options: nosniff
D/OkHttp: Date: Tue, 02 Aug 2016 17:39:23 GMT
D/OkHttp: X-Pagination: {"total":34,"total_pages":2,"first_page":true,"last_page":false,"prev_page":null,"next_page":2,"out_of_range":false}
D/OkHttp: Cache-Control: max-age=10800, public, no-transform
D/OkHttp: Etag: W/"4dcf69c9456102fd57666a1dff0eec3a"
D/OkHttp: X-Request-Id: 1fb917ac-7f77-4c99-8a3b-20d56af9d441
D/OkHttp: X-Runtime: 0.081711
D/OkHttp: Via: 1.1 vegur

我的json响应缓存标题如下:

提前致谢,

1 个答案:

答案 0 :(得分:0)

您没有明确提到使用ETag。但是由于它们的使用,我遇到了同样的问题。所以它可能是相关的。

OkHttp支持ETag的一个缺点是OkHttp永远不会“刷新”缓存的到期时间。因此,一旦它过期,它将永远不会使用缓存,直到它被更新。只有在资源更新后才会出现这种情况(etags不同,并且返回200响应,而304)。这意味着OkHttp客户端将继续为每个后续请求转到网络,只要它继续获得304响应。 HTTP规范对客户端应如何处理这种情况很模糊,所以我不认为这是一个错误。但如果我继续打网络,它确实会破坏缓存的目的。我提出的唯一解决方案是在我知道缓存已过期并需要“刷新”时提供“无缓存”缓存控制头。这将检索响应(假设它是200)并刷新缓存及其到期。

以下是一种方法。大纲:

  1. 强制客户端缓存(在我的情况下,服务器发送必须无效的缓存控制头)
  2. 记录带有日期+最大年龄的网址以充当内部缓存
  3. 听取请求。如果请求不包含缓存的URL(第一次或应用程序重新启动)或URL已过期,请在请求中发送无缓存的Cache-Control标头。这会强制请求绕过缓存和If-Not-Modified条件。然后OkHttp在内部缓存响应,我们之前在步骤2中的拦截器捕获它并更新我们的内部URL缓存。
  4. 这种方法的一个缺点是我们使用no-cache强制刷新。这意味着即使内容与已缓存的内容相同,服务器也将最终提供内容。在我的情况下,这是一个可接受的权衡,因为服务器正在处理请求所有相同(只是为了生成ETag哈希)。所以条件if-none-match只是保存了有效载荷传输而不是Web服务器处理。在我的情况下,服务器处理几乎占据了所有性能损失(因此我需要首先强制缓存)。

    以下是一些代码:

    <!-- language: kotlin -->
    class RestAdapterFactory(private val app: Context) {
    
        private var httpClient: OkHttpClient.Builder? = null
        private var builder: Retrofit.Builder? = null
        private val MAX_AGE = 60 * 1
        private val READ_TIMEOUT = 30.toLong()
        private val CACHE_SIZE = 5 * 1024 * 1024.toLong() // 5 MB
        private val apiCache: MutableMap<String, DateTime> = HashMap()
    
        private fun getHttpClient(): OkHttpClient.Builder {
            if (httpClient == null) {
                httpClient = OkHttpClient.Builder()
                        .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
                        .cache(Cache(app.cacheDir, CACHE_SIZE))
                        .addNetworkInterceptor(getWebApiResponseInterceptor())
                        .addInterceptor(getConditionalCacheRequestInterceptor())
            }
            return httpClient!!
        }
    
        /***
         * Stores url entry with time to expire (to be used in conjunction with [getConditionalCacheRequestInterceptor]
         */
        private fun getWebApiResponseInterceptor(): (Interceptor.Chain) -> Response {
            return {
                val response = it.proceed(it.request())
                apiCache[it.request().url().toString()] = DateTime().plusSeconds(MAX_AGE)
                response.newBuilder().header("Cache-Control", "max-age=$MAX_AGE").build() // forcing client to cache
            }
        }
    
        /***
         * Checks expiration of url, if url exists and is expired then force a response with no-cache
         */
        private fun getConditionalCacheRequestInterceptor(): (Interceptor.Chain) -> Response {
            return {
                val original = it.request()
                val urlExpiration = apiCache[original.url().toString()]
                val noCache = urlExpiration == null || urlExpiration < DateTime()
                it.proceed(if (noCache) original.newBuilder().header("Cache-Control", "no-cache").build() else original)
            }
        }
    
    }