离线时可以使用 OKHttp 进行改造吗?
Posted
技术标签:
【中文标题】离线时可以使用 OKHttp 进行改造吗?【英文标题】:Can Retrofit with OKHttp use cache data when offline 【发布时间】:2014-06-19 04:35:42 【问题描述】:我正在尝试使用 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 响应和标头在那里。但是当我尝试离线请求时,我总是得到RetrofitError UnknownHostException
。我还应该做些什么来让 Retrofit 从缓存中读取响应?
编辑:
由于 OKHttp 2.0.x HttpResponseCache
是 Cache
,setResponseCache
是 setCache
【问题讨论】:
您调用的服务器是否使用适当的 Cache-Control 标头进行响应? 返回这个Cache-Control: s-maxage=1209600, max-age=1209600
不知道够不够。
似乎public
关键字需要在响应标头中才能使其脱机工作。但是,这些标头不允许 OkClient 在可用时使用网络。是否有设置缓存策略/策略以在可用时使用网络?
我不确定您是否可以在同一个请求中执行此操作。您可以检查相关的 CacheControl 类和 Cache-Control 标头。如果没有这样的行为,我可能会选择发出两个请求,一个仅缓存的请求(only-if-cached),然后是一个网络(max-age=0)一个。
这是我想到的第一件事。我在 CacheControl 和 CacheStrategy 类中度过了几天。但是两个请求的想法没有多大意义。如果max-stale + max-age
被传递,它确实从网络请求。但我想设置 max-stale 一周。即使有可用的网络,它也会从缓存中读取响应。
【参考方案1】:
为 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 = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
同时查看 @kosiara - Bartosz Kosarzycki 的 answer。您可能需要从响应中删除一些标头。
OKHttp 2.0.x(查看原答案):
由于 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
请求标头。 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);
);
【讨论】:
提个建议:HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...)
.
当网络不可用时,我没有调用拦截器。我不确定网络不可用时的情况会如何发生。我在这里错过了什么吗?
是 if (Utils.isNetworkAvailable(context))
正确还是应该颠倒,即 if (!Utils.isNetworkAvailable(context))
?
我正在使用 Retrofit 2.1.0,当手机离线时,public okhttp3.Response intercept(Chain chain) throws IOException
永远不会被呼叫,只有在我在线时才会被呼叫
这里的数据只在'max-age'时间内离线可用,60秒后无法加载。 @StarWars 有什么想法吗?【参考方案2】:
上面所有的答案都不适合我。我尝试在 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();
【讨论】:
看起来你的interceptors ()
和networkInterceptors ()
是一样的。你为什么要复制这个?
在这里阅读不同类型的拦截器。 github.com/square/okhttp/wiki/Interceptors
是的,但它们都做同样的事情,所以我很确定 1 个拦截器就足够了,对吧?
是否有特定原因不对.networkInterceptors().add()
和interceptors().add()
使用相同的拦截器实例?【参考方案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();
【讨论】:
不适合我...获取 504 Unsatisfiable Request (only-if-cached) 只有你的解决方案对我有帮助,非常感谢。浪费 2 天向下滚动 是的,就我而言,这是唯一可行的解决方案。 (改造 1.9.x + okHttp3) 适用于改造 RETROFIT_VERSION=2.2.0 OKHTTP_VERSION=3.6.0 如何在该方法中添加builder.addheader()来授权访问api?【参考方案4】:答案是肯定的,基于以上答案,我开始编写单元测试来验证所有可能的用例:
离线时使用缓存 先使用缓存响应直到过期,然后再使用网络 先使用网络,然后缓存一些请求 不要将某些响应存储在缓存中我建立了一个小的帮助库来轻松配置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);
如您所见,缓存即使过期也可以使用。 希望它会有所帮助。
【讨论】:
你的库很棒!感谢您的辛勤工作。库:github.com/ncornette/OkCacheControl【参考方案5】:在@kosiara-bartosz-kasarzycki 的answer 的基础上,我创建了一个示例项目,该项目使用改造、okhttp、rxjava 和番石榴从内存->磁盘->网络正确加载。 https://github.com/digitalbuddha/StoreDemo
【讨论】:
【参考方案6】:使用 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
【讨论】:
当我第一次使用离线模式加载时,它崩溃了!否则它工作正常 这对我不起作用。在我尝试整合它的原理后,我复制粘贴它并尝试过它,但确实让它工作。 App.sApp.getCacheDir() 这是做什么的?【参考方案7】:注意! OkHttp 内置缓存仅适用于GET
方法(参考上述解决方案)。如果要缓存POST
请求,必须自己实现。
【讨论】:
以上是关于离线时可以使用 OKHttp 进行改造吗?的主要内容,如果未能解决你的问题,请参考以下文章