当使用Retrofit和OKHttp时,是否可以在离线时使用缓存数据?

158

我正在尝试使用Retrofit和OKHttp缓存HTTP响应。我遵循了这个代码片段,最终得到了以下代码:

File httpCacheDirectory = new File(context.getCacheDir(), "responses");

HttpResponseCache httpResponseCache = null;
try {
     httpResponseCache = new HttpResponseCache(httpCacheDirectory, 10 * 1024 * 1024);
} catch (IOException e) {
     Log.e("Retrofit", "Could not create http cache", e);
}

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setResponseCache(httpResponseCache);

api = new RestAdapter.Builder()
          .setEndpoint(API_URL)
          .setLogLevel(RestAdapter.LogLevel.FULL)
          .setClient(new OkClient(okHttpClient))
          .build()
          .create(MyApi.class);

这是带有Cache-Control头的MyApi。

public interface MyApi {
   @Headers("Cache-Control: public, max-age=640000, s-maxage=640000 , max-stale=2419200")
   @GET("/api/v1/person/1/")
   void requestPerson(
           Callback<Person> callback
   );

首先,我通过在线请求并检查缓存文件。正确的JSON响应和标头都存在。但是当我尝试离线请求时,我总是会收到RetrofitError UnknownHostException错误。是否还有其他事情我需要做才能使Retrofit从缓存中读取响应?

编辑: 自OKHttp 2.0.x以来,HttpResponseCache已更名为Cache,而setResponseCache已更名为setCache


1
你所调用的服务器是否响应了适当的Cache-Control头? - Hassan Ibraheem
似乎需要在响应头中使用public关键字才能使其离线工作。但是,这些标头不允许OkClient在有网络可用时使用网络。是否有任何方法可以设置缓存策略/策略以在有网络可用时使用网络? - osrl
我不确定你是否可以在同一个请求中完成这个操作。你可以查看相关的CacheControl类和Cache-Control头文件。如果没有这样的行为,我可能会选择进行两个请求,一个是仅缓存请求(only-if-cached),然后是一个网络请求(max-age=0)。 - Hassan Ibraheem
那是我脑海中浮现的第一件事。我在CacheControl和CacheStrategy类中花了好几天的时间。但是两个请求的想法并没有太多意义。如果传递了“max-stale + max-age”,它会从网络发出请求。但我想将max-stale设置为一周。这使得即使有网络可用,它也会从缓存中读取响应。 - osrl
也可以查看服务器的头文件,例如此处:https://dev59.com/xV0Z5IYBdhLWcg3wiw4w#31606496 - Gelldur
显示剩余3条评论
7个回答

198

Retrofit 2.x的编辑:

在离线时,OkHttp拦截器是访问缓存的正确方法:

1)创建拦截器:

private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());
        if (Utils.isNetworkAvailable(context)) {
            int maxAge = 60; // read from cache for 1 minute
            return originalResponse.newBuilder()
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .build();
        } else {
            int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
            return originalResponse.newBuilder()
                    .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                    .build();
        }
    }

2) 设置客户端:

OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(REWRITE_CACHE_CONTROL_INTERCEPTOR);

//setup cache
File httpCacheDirectory = new File(context.getCacheDir(), "responses");
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(httpCacheDirectory, cacheSize);

//add cache to the client
client.setCache(cache);

3) 将客户端添加到 Retrofit

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build();

请查看@kosiara - Bartosz Kosarzycki回答。您可能需要从响应中删除一些头信息。

OKHttp 2.0.x(请查看原始答案):

自OKHttp 2.0.x以来,HttpResponseCache已更名为CachesetResponseCache也更名为setCache。因此,您应该像这样设置Cache

        File httpCacheDirectory = new File(context.getCacheDir(), "responses");

        Cache cache = null;
        try {
            cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
        } catch (IOException e) {
            Log.e("OKHttp", "Could not create http cache", e);
        }

        OkHttpClient okHttpClient = new OkHttpClient();
        if (cache != null) {
            okHttpClient.setCache(cache);
        }
        String hostURL = context.getString(R.string.host_url);

        api = new RestAdapter.Builder()
                .setEndpoint(hostURL)
                .setClient(new OkClient(okHttpClient))
                .setRequestInterceptor(/*rest of the answer here */)
                .build()
                .create(MyApi.class);

最初的回答:

事实证明,服务器响应必须具有Cache-Control: public才能使OkClient从缓存中读取。

另外,如果您希望在网络可用时从网络请求,则应添加Cache-Control: max-age=0请求头。 此答案显示了如何进行参数化设置。这就是我使用它的方式:

RestAdapter.Builder builder= new RestAdapter.Builder()
   .setRequestInterceptor(new RequestInterceptor() {
        @Override
        public void intercept(RequestFacade request) {
            request.addHeader("Accept", "application/json;versions=1");
            if (MyApplicationUtils.isNetworkAvailable(context)) {
                int maxAge = 60; // read from cache for 1 minute
                request.addHeader("Cache-Control", "public, max-age=" + maxAge);
            } else {
                int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                request.addHeader("Cache-Control", 
                    "public, only-if-cached, max-stale=" + maxStale);
            }
        }
});

2
只是一个建议:HttpResponseCache已经更名为Cache。请使用OkHttpClient.setCache(...)安装它,而不是OkHttpClient.setResponseCache(...) - Henrique de Sousa
2
当网络不可用时,我没有拦截器被调用。我不确定什么情况下会出现网络不可用的条件。我在这里漏掉了什么吗? - Androidme
2
if (Utils.isNetworkAvailable(context)) 是正确的吗?还是应该反过来写,即 if (!Utils.isNetworkAvailable(context)) - ericn
2
我正在使用Retrofit 2.1.0,当手机离线时,public okhttp3.Response intercept(Chain chain) throws IOException 没有被调用,只有在在线时才会被调用。 - ericn
1
这里的数据仅在“max-age”时间内离线可用,60秒后无法加载。@StarWars 有什么想法吗? - Boy
显示剩余16条评论

32

以上所有答案都对我没有用。我尝试在retrofit 2.0.0-beta2中实现离线缓存。我使用okHttpClient.networkInterceptors()方法添加了一个拦截器,但是当我尝试脱机使用缓存时收到java.net.UnknownHostException。事实证明,我还需要添加okHttpClient.interceptors()

问题在于缓存未写入Flash存储器,因为服务器返回了Pragma:no-cache,这会防止OkHttp存储响应。即使修改请求头值后,离线缓存仍然无法正常工作。经过一些试验和错误,我通过从响应中删除Pragma而不是请求来使缓存正常工作 - response.newBuilder().removeHeader("Pragma");

Retrofit:2.0.0-beta2;OkHttp:2.5.0

OkHttpClient okHttpClient = createCachedClient(context);
Retrofit retrofit = new Retrofit.Builder()
        .client(okHttpClient)
        .baseUrl(API_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build();
service = retrofit.create(RestDataResource.class);

...

private OkHttpClient createCachedClient(final Context context) {
    File httpCacheDirectory = new File(context.getCacheDir(), "cache_file");

    Cache cache = new Cache(httpCacheDirectory, 20 * 1024 * 1024);
    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.setCache(cache);
    okHttpClient.interceptors().add(
            new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request originalRequest = chain.request();
                    String cacheHeaderValue = isOnline(context) 
                        ? "public, max-age=2419200" 
                        : "public, only-if-cached, max-stale=2419200" ;
                    Request request = originalRequest.newBuilder().build();
                    Response response = chain.proceed(request);
                    return response.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", cacheHeaderValue)
                        .build();
                }
            }
    );
    okHttpClient.networkInterceptors().add(
            new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request originalRequest = chain.request();
                    String cacheHeaderValue = isOnline(context) 
                        ? "public, max-age=2419200" 
                        : "public, only-if-cached, max-stale=2419200" ;
                    Request request = originalRequest.newBuilder().build();
                    Response response = chain.proceed(request);
                    return response.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", cacheHeaderValue)
                        .build();
                }
            }
    );
    return okHttpClient;
}

...

public interface RestDataResource {

    @GET("rest-data") 
    Call<List<RestItem>> getRestData();

}

7
你的 interceptors()networkInterceptors() 看起来是相同的。为什么会重复这样做? - Etienne Lawlor
不同类型的拦截器在这里阅读。https://github.com/square/okhttp/wiki/Interceptors - Rohit Bandil
是的,但它们都做相同的事情,所以我相信一个拦截器就足够了,对吗? - Ovidiu Latcu
有没有特定的原因不使用相同的拦截器实例来同时添加.networkInterceptors().add()interceptors().add() - ccpizza

22

我的解决方案:

private BackendService() {

    httpCacheDirectory = new File(context.getCacheDir(),  "responses");
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(httpCacheDirectory, cacheSize);

    httpClient = new OkHttpClient.Builder()
            .addNetworkInterceptor(REWRITE_RESPONSE_INTERCEPTOR)
            .addInterceptor(OFFLINE_INTERCEPTOR)
            .cache(cache)
            .build();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.backend.com")
            .client(httpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    backendApi = retrofit.create(BackendApi.class);
}

private static final Interceptor REWRITE_RESPONSE_INTERCEPTOR = chain -> {
    Response originalResponse = chain.proceed(chain.request());
    String cacheControl = originalResponse.header("Cache-Control");

    if (cacheControl == null || cacheControl.contains("no-store") || cacheControl.contains("no-cache") ||
            cacheControl.contains("must-revalidate") || cacheControl.contains("max-age=0")) {
        return originalResponse.newBuilder()
                .header("Cache-Control", "public, max-age=" + 10)
                .build();
    } else {
        return originalResponse;
    }
};

private static final Interceptor OFFLINE_INTERCEPTOR = chain -> {
    Request request = chain.request();

    if (!isOnline()) {
        Log.d(TAG, "rewriting request");

        int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
        request = request.newBuilder()
                .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                .build();
    }

    return chain.proceed(request);
};

public static boolean isOnline() {
    ConnectivityManager cm = (ConnectivityManager) MyApplication.getApplication().getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo netInfo = cm.getActiveNetworkInfo();
    return netInfo != null && netInfo.isConnectedOrConnecting();
}

3
对我来说无法运作……出现了 504 不可满足请求(仅在缓存时)的错误。 - zacharia
只有你的解决方案能帮助我,非常感谢。 浪费了两天时间向下滚动。 - Ярослав Мовчан
1
是的,在我的情况下,这是唯一可行的解决方案。(Retrofit 1.9.x + okHttp3) - Hoang Nguyen Huu
1
适用于 Retrofit RETROFIT_VERSION=2.2.0 OKHTTP_VERSION=3.6.0。 - Tadas Valaitis
如何在此方法中添加 builder.addheader() 以使用授权访问 API? - Abhilash

7
根据以上回答,答案是肯定的。我开始编写单元测试来验证所有可能的用例:
  • 离线时使用缓存
  • 首先使用缓存响应,直到过期,然后使用网络
  • 对于某些请求,首先使用网络,然后使用缓存
  • 不要将某些响应存储在缓存中
我构建了一个小型帮助库来轻松配置OKHttp缓存,您可以在Github上查看相关的单元测试:https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ncornette/cache/OkCacheControlTest.java 演示离线使用缓存的单元测试:
@Test
public void test_USE_CACHE_WHEN_OFFLINE() throws Exception {
    //given
    givenResponseInCache("Expired Response in cache", -5, MINUTES);
    given(networkMonitor.isOnline()).willReturn(false);

    //when
    //This response is only used to not block when test fails
    mockWebServer.enqueue(new MockResponse().setResponseCode(404));
    Response response = getResponse();

    //then
    then(response.body().string()).isEqualTo("Expired Response in cache");
    then(cache.hitCount()).isEqualTo(1);
}

正如你所看到的,即使缓存过期了也可以使用。希望这能帮到你。


2
你的库非常棒!感谢你的辛勤工作。 该库: https://github.com/ncornette/OkCacheControl - Hoang Nguyen Huu

6

2

使用Retrofit2和OkHTTP3进行缓存:

OkHttpClient client = new OkHttpClient
  .Builder()
  .cache(new Cache(App.sApp.getCacheDir(), 10 * 1024 * 1024)) // 10 MB
  .addInterceptor(new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
      Request request = chain.request();
      if (NetworkUtils.isNetworkAvailable()) {
        request = request.newBuilder().header("Cache-Control", "public, max-age=" + 60).build();
      } else {
        request = request.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7).build();
      }
      return chain.proceed(request);
    }
  })
  .build();

NetworkUtils.isNetworkAvailable() 静态方法:

public static boolean isNetworkAvailable(Context context) {
        ConnectivityManager cm =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return activeNetwork != null &&
                activeNetwork.isConnectedOrConnecting();
    }

然后只需将客户端添加到Retrofit构建器中:
Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

原始来源:https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html

这篇文章介绍了如何在Android应用中使用Retrofit2和OkHttp3库来实现缓存网络请求的离线功能。通过使用OkHttp3缓存,我们可以在没有网络连接的情况下仍然能够获取到之前请求过的数据。本文提供了一个完整的代码示例,帮助读者更好地理解如何实现这一功能。


1
当我首次使用离线模式加载时,它会崩溃!否则它可以正常工作。 - Zafer Celaloglu
这对我不起作用。我复制粘贴了它,并在尝试整合其原理后进行了尝试,但无法使其正常工作。 - Boy
2
App.sApp.getCacheDir() 这是什么意思? - Huzaifa Asif

1

注意!OkHttp内置缓存仅适用于GET方法(请参考上面的解决方案)。如果您想缓存POST请求,则必须自己实现。 在此输入图像描述


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接