【问题标题】:Retrofit 2: Caching doesn't work after cached response expires改造 2:缓存响应过期后缓存不起作用
【发布时间】:2016-12-06 01:08:44
【问题描述】:

在我的 android 应用程序中,我使用的是 Retrofit 2 和捆绑的 okhttp。 我正在使用以下代码来设置缓存

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 响应缓存标头如下:

提前致谢,

【问题讨论】:

  • @JesseWilson 你提到的测试文件通过了。但在真实设备上,我的应用程序仍然存在此问题。改造 1.9 和 okhttp3 运行良好。
  • @JesseWilson 我也添加了响应日志。你能帮忙解决这个问题吗?
  • 尝试使用您的调试器并单步执行缓存代码。
  • @JesseWilson 我浏览了缓存目录中的 shell 和 cd。它也创建了 .0 和 .1 文件,其中一个是可读的并且具有标题,而另一个则完全是胡言乱语。并且日志文件有条目一个被标记为干净,另一个被标记为脏。

标签: android caching retrofit okhttp okhttp3


【解决方案1】:

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

OkHttp 支持 ETags 的一个缺点是 OkHttp 从不“刷新”缓存的过期时间。所以一旦过期,它就永远不会使用缓存,直到它被更新。仅当资源已更新时才会发生这种情况(etag 不同并且返回 200 响应与 304)。这意味着 OkHttp 客户端只要继续得到 304 响应,就会继续为每个后续请求访问网络。 HTTP 规范对客户端应如何处理这种情况含糊不清,所以我不认为这是一个错误。但是,如果我继续访问网络,它确实会破坏缓存的目的。我想出的唯一解决方案是在我知道缓存已过期并需要“刷新”时提供“无缓存”Cache-Control 标头。这将检索响应(假设它是 200)并刷新缓存及其过期时间。

以下是一种方法作为示例。大纲:

  1. 强制客户端缓存(在我的例子中,服务器发送 must-invalidate 缓存控制标头)
  2. 使用 date+max-age 记录 url 以充当内部缓存
  3. 监听请求。如果请求不包含缓存的 URL(首次或应用重启)或 URL 已过期,请在请求中发送无缓存 Cache-Control 标头。这会强制请求绕过缓存和 If-Not-Modified 条件得到服务。然后,OkHttp 会在内部缓存响应,我们之前在第 2 步中的拦截器会捕获它并更新我们的内部 URL 缓存。

这种方法的一个缺点是我们使用无缓存强制刷新。这意味着即使内容与已经缓存的内容相同,服务器最终也会提供内容。在我的情况下,这是一个可以接受的折衷方案,因为服务器处理请求的方式都是一样的(只是为了生成 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)
        }
    }

}

【讨论】:

    猜你喜欢
    • 2018-02-04
    • 2018-05-16
    • 2016-02-18
    • 1970-01-01
    • 2016-03-20
    • 2011-11-08
    • 2019-03-25
    • 2015-06-01
    • 2018-01-18
    相关资源
    最近更新 更多