首页 文章

使用OKHttp进行改造可以在离线时使用缓存数据

提问于
浏览
138

我正在尝试使用Retrofit和OKHttp来缓存HTTP响应 . 我跟着this gist,最后得到了这段代码:

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响应和 Headers . 但是当我尝试离线请求时,我总是得到 RetrofitError UnknownHostException . 我还有什么办法让Retrofit从缓存中读取响应吗?

EDIT: 由于OKHttp 2.0.x HttpResponseCacheCachesetResponseCachesetCache

6 回答

  • 173

    编辑改造2.x:

    OkHttp Interceptor是离线时访问缓存的正确方法:

    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 = new Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build();
    

    另请查看 @kosiara - Bartosz Kosarzyckianswer . 您可能需要从响应中删除一些标头 .


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

    由于OKHttp 2.0.x HttpResponseCacheCachesetResponseCachesetCache . 所以你应该 setCache 这样:

    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 请求标头 . This answer显示了如何进行参数化 . 这就是我使用它的方式:

    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);
                }
            }
    });
    
  • 5

    以上所有的东西都不适合我 . 我试图在 retrofit 2.0.0-beta2 中实现离线缓存 . 我使用 okHttpClient.networkInterceptors() 方法添加了一个拦截器,但是当我尝试脱机使用缓存时收到 java.net.UnknownHostException . 事实证明我也必须添加 okHttpClient.interceptors() .

    问题是缓存没有写入闪存存储,因为服务器返回了 Pragma:no-cache ,这阻止了OkHttp存储响应 . 即使在修改请求标头值后,脱机缓存也不起作用 . 经过一些试验和错误后,我通过从响应中删除pragma而不是请求来使缓存工作而不修改后端端 - response.newBuilder().removeHeader("Pragma");

    改造: 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();
    
    }
    
  • 3

    我的解决方案

    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();
    }
    
  • 28

    Build 在@ kosiara-bartosz-kasarzycki的answer上,我创建了一个示例项目,使用改造,okhttp,rxjava和guava从内存 - >磁盘 - >网络正确加载 . https://github.com/digitalbuddha/StoreDemo

  • 1

    答案是肯定的,根据上述答案,我开始编写单元测试来验证所有可能的用例:

    • Use cache when offline

    • 首先使用缓存响应,直到过期,然后使用网络

    • 首先使用网络然后缓存某些请求

    • 不要在缓存中存储某些响应

    我构建了一个小助手lib来轻松配置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);
    }
    

    如您所见,即使缓存已过期,也可以使用缓存 . 希望它会有所帮助 .

  • 21

    使用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 = new Retrofit.Builder()
                        .baseUrl(BASE_URL)
                        .client(client)
                        .addConverterFactory(GsonConverterFactory.create())
                        .build();
    

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

相关问题