我正在尝试使用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( Callbackcallback );
首先,我在线请求并检查缓存文件.有正确的JSON响应和标题.但是当我尝试离线请求时,我总是得到RetrofitError UnknownHostException
.我还有什么办法让Retrofit从缓存中读取响应吗?
编辑:
因为OKHttp 2.0.x HttpResponseCache
是Cache
,setResponseCache
是setCache
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 Kosarzycki的回答.您可能需要从响应中删除一些标头.
因为OKHttp 2.0.x HttpResponseCache
是Cache
,setResponseCache
是setCache
.所以你应该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
请求标头.这个答案显示了如何进行参数化.这就是我使用它的方式:
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.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(); }
建立在@ kosiara-bartosz-kasarzycki的回答之上,我创建了一个示例项目,使用改造,okhttp,rxjava和guava从内存 - >磁盘 - >网络正确加载. https://github.com/digitalbuddha/StoreDemo
我的解决方案
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(); }
答案是肯定的,基于上述答案,我开始编写单元测试以验证所有可能的用例:
离线使用缓存
首先使用缓存的响应,直到过期,然后使用网络
首先使用网络,然后缓存一些请求
不要在缓存中存储某些响应
我构建了一个小的帮助程序库来轻松配置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); }
如您所见,即使缓存已过期也可以使用。希望它会有所帮助。