OKHttp 可以在脱机时使用缓存数据进行改进

我试图使用 Revifit & 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);

这是带缓存控制头的 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。还有什么我应该做的,使逆转修复读从缓存的响应?

编辑: 因为 OKHttp 2.0.x HttpResponseCacheCache,所以 setResponseCachesetCache

97737 次浏览

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

还要检查 @ kosiara-Bartosz Kosarzycki回答。您可能需要从响应中删除一些头部。


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

因为 OKHttp 2.0.x HttpResponseCacheCachesetResponseCachesetCache,所以你应该这样做:

        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-Beta 2中实现离线缓存。我使用 okHttpClient.networkInterceptors()方法添加了一个拦截器,但在尝试脱机使用缓存时收到了 java.net.UnknownHostException。结果我还必须添加 okHttpClient.interceptors()

问题在于缓存没有写入闪存,因为服务器返回了 Pragma:no-cache,这阻止了 OkHttp 存储响应。即使修改了请求头值,脱机缓存也无法工作。经过一番反复试验之后,我在不修改后端的情况下使缓存正常工作,方法是从 response 中删除杂注,而不是从 request-response.newBuilder().removeHeader("Pragma");中删除杂注

翻新: 2.0.0-beta2; OkHttp: 2.5

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 的 回答的基础上,我创建了一个示例项目,它使用 rolfit、 okhttp、 rxjava 和番石榴从内存-> 磁盘-> 网络正确加载。 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();
}

使用 Revifit2和 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();

IsNetworkCompleable ()静态方法:

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

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

  • 离线时使用缓存
  • 首先使用缓存的响应,直到过期,然后网络
  • 首先使用网络,然后缓存一些请求
  • 不要将某些响应存储在缓存中

我构建了一个小的 helper 库来轻松配置 OKHttp 缓存,你可以在 Github 上看到相关的单元测试: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ncornette/cache/OkCacheControlTest.java

演示离线时如何使用缓存的 Unittest:

@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);
}

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

立正!OkHttp 内建缓存只适用于 GET方法(参见上述解决方案)。如果要缓存 POST请求,必须自己实现。 enter image description here