改造 + OkHTTP - 响应缓存不工作
Retrofit + OkHTTP - response cache not working
我知道有很多类似的问题,但我已经全部阅读了,其中 none 真的很有帮助。
所以,这是我的问题:
我正在使用改造 + okhttp 从 API 获取一些数据,我想缓存它们。不幸的是,我没有 API 服务器的管理员权限,所以我无法修改服务器返回的 headers。 (当前,服务器 returns Cache-control:私有)
所以我决定使用 okhttp header 欺骗来插入适当的缓存 headers。可悲的是,无论我做什么,缓存似乎都不起作用。
我这样初始化 api 服务:
int cacheSize = 10 * 1024 * 1024; // 10 MiB
File cacheFile = new File(context.getCacheDir(), "thumbs");
final Cache cache = new Cache(cacheFile, cacheSize);
OkHttpClient client = new OkHttpClient();
client.setCache(cache);
client.interceptors().add(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.removeHeader("Access-Control-Allow-Origin")
.removeHeader("Vary")
.removeHeader("Age")
.removeHeader("Via")
.removeHeader("C3-Request")
.removeHeader("C3-Domain")
.removeHeader("C3-Date")
.removeHeader("C3-Hostname")
.removeHeader("C3-Cache-Control")
.removeHeader("X-Varnish-back")
.removeHeader("X-Varnish")
.removeHeader("X-Cache")
.removeHeader("X-Cache-Hit")
.removeHeader("X-Varnish-front")
.removeHeader("Connection")
.removeHeader("Accept-Ranges")
.removeHeader("Transfer-Encoding")
.header("Cache-Control", "public, max-age=60")
//.header("Expires", "Mon, 27 Apr 2015 08:15:14 GMT")
.build();
}
});
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(API_ROOT)
.setLogLevel(RestAdapter.LogLevel.HEADERS_AND_ARGS)
.setClient(new OkClient(client))
.setConverter(new SimpleXMLConverter(false))
.setRequestInterceptor(new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
if (Network.isConnected(context)) {
int maxAge = 60; // read from cache for 2 minutes
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);
}
}
})
.build();
api = restAdapter.create(ApiService.class);
当然,没有必要删除所有这些 headers,但我想让响应尽可能干净,以排除这些额外 headers 的一些干扰。
如您所见,我还尝试欺骗 Expires 和 Date header(我尝试删除它们,设置它们以便它们之间完全存在 max-age 差异并将 Expires 设置为远未来)。我还尝试了各种 Cache-control 值,但没有成功。
我确保 cacheFile 存在,isDirectory 并且可由应用程序写入。
这些是改造直接记录的请求和响应 header:
Request:
Cache-Control: public, max-age=60
---> END HTTP (no body)
Response:
Date: Mon, 27 Apr 2015 08:41:10 GMT
Server: Apache/2.2.22 (Ubuntu)
Expires: Mon, 27 Apr 2015 08:46:10 GMT
Content-Type: text/xml; charset=UTF-8
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1430124070000
OkHttp-Received-Millis: 1430124070040
Cache-Control: public, max-age=60
<--- END HTTP (-1-byte body)
<--- BODY: ...
最后还有一件奇怪的事情:在某个时候,缓存工作了几分钟。我得到了合理的命中数,甚至离线请求也返回了缓存值。 (它发生在使用此处发布的确切设置时)但是当我重新启动应用程序时,一切都回到了 "normal"(持续命中计数 0)。
如果有人知道这里可能存在什么问题,我将非常乐意提供任何帮助:)
使用 networkInterceptors() 而不是 interceptors()。结合您删除任何与缓存相关的 headers 的策略将起作用。这是简短的回答。
当您使用拦截器更改 headers 时,它不会在调用 CacheStrategy.isCacheable() 之前进行任何调整。值得查看 CacheStrategy 和 CacheControl 类 以了解 OKHttp 如何处理 cache-related headers。在 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
上执行 ctrl+f "cache" 也是值得的
我不确定 networkInterceptors() 和 interceptors() 文档是否不清楚或者是否存在错误。一旦我进一步研究,我会更新这个答案。
这里还有一件事要补充,除了 Brendan Weinstein 的回答只是为了确认 OkHttp3 缓存不适用于 post 请求。
一整天后,我发现我的离线缓存不起作用只是因为我在API类型中使用了POST
。我将其更改为 GET
的那一刻,它起作用了!
@GET("/ws/audioInactive.php")
Call<List<GetAudioEntity>> getAudios();
我的整个 Retrofit class。
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.limnet.iatia.App;
import com.limnet.iatia.netio.entity.registration.APIInterfaceProviderIMPL;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RHTRetroClient {
public static final String BASE_URL = "https://abc.pro";
private static Retrofit retrofit = null;
private static RHTRetroClient mInstance;
private static final long cacheSize = 10 * 1024 * 1024; // 10 MB
public static final String HEADER_CACHE_CONTROL = "Cache-Control";
public static final String HEADER_PRAGMA = "Pragma";
private RHTRetroClient() {
Gson gson = new GsonBuilder()
.setLenient()
.create();
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
Cache cache = new Cache(new File(App.getAppContext().getCacheDir(), "soundbites"),cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(httpLoggingInterceptor()) // used if network off OR on
.addNetworkInterceptor(networkInterceptor()) // only used when network is on
.addInterceptor(offlineInterceptor())
.build();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
/**
* This interceptor will be called both if the network is available and if the network is not available
*
* @return
*/
private static Interceptor offlineInterceptor() {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Log.d("rht", "offline interceptor: called.");
Request request = chain.request();
// prevent caching when network is on. For that we use the "networkInterceptor"
if (!App.hasNetwork()) {
CacheControl cacheControl = new CacheControl.Builder()
.maxStale(7, TimeUnit.DAYS)
.build();
request = request.newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.cacheControl(cacheControl)
.build();
}
return chain.proceed(request);
}
};
}
/**
* This interceptor will be called ONLY if the network is available
*
* @return
*/
private static Interceptor networkInterceptor() {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Log.d("rht", "network interceptor: called.");
Response response = chain.proceed(chain.request());
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(5, TimeUnit.SECONDS)
.build();
return response.newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.header(HEADER_CACHE_CONTROL, cacheControl.toString())
.build();
}
};
}
private static HttpLoggingInterceptor httpLoggingInterceptor() {
HttpLoggingInterceptor httpLoggingInterceptor =
new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Log.d("rht", "log: http log: " + message);
}
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return httpLoggingInterceptor;
}
public static synchronized RHTRetroClient getInstance() {
if (mInstance == null) {
mInstance = new RHTRetroClient();
}
return mInstance;
}
public APIInterfaceProviderIMPL getAPIInterfaceProvider() {
return retrofit.create(APIInterfaceProviderIMPL.class);
}
}
检查您的回复中是否有 Pragma header。如果 Pragma: no-cache
header 存在,使用 max-age
的缓存将不起作用。
如果确实有 Pragma
header,请在 Interceptor
中执行以下操作将其删除:
override fun intercept(chain: Interceptor.Chain): Response {
val cacheControl = CacheControl.Builder()
.maxAge(1, TimeUnit.MINUTES)
.build()
return originalResponse.newBuilder()
.header("Cache-Control", cacheControl.toString())
.removeHeader("Pragma") // Caching doesnt work if this header is not removed
.build()
}
我知道有很多类似的问题,但我已经全部阅读了,其中 none 真的很有帮助。
所以,这是我的问题:
我正在使用改造 + okhttp 从 API 获取一些数据,我想缓存它们。不幸的是,我没有 API 服务器的管理员权限,所以我无法修改服务器返回的 headers。 (当前,服务器 returns Cache-control:私有)
所以我决定使用 okhttp header 欺骗来插入适当的缓存 headers。可悲的是,无论我做什么,缓存似乎都不起作用。
我这样初始化 api 服务:
int cacheSize = 10 * 1024 * 1024; // 10 MiB
File cacheFile = new File(context.getCacheDir(), "thumbs");
final Cache cache = new Cache(cacheFile, cacheSize);
OkHttpClient client = new OkHttpClient();
client.setCache(cache);
client.interceptors().add(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.removeHeader("Access-Control-Allow-Origin")
.removeHeader("Vary")
.removeHeader("Age")
.removeHeader("Via")
.removeHeader("C3-Request")
.removeHeader("C3-Domain")
.removeHeader("C3-Date")
.removeHeader("C3-Hostname")
.removeHeader("C3-Cache-Control")
.removeHeader("X-Varnish-back")
.removeHeader("X-Varnish")
.removeHeader("X-Cache")
.removeHeader("X-Cache-Hit")
.removeHeader("X-Varnish-front")
.removeHeader("Connection")
.removeHeader("Accept-Ranges")
.removeHeader("Transfer-Encoding")
.header("Cache-Control", "public, max-age=60")
//.header("Expires", "Mon, 27 Apr 2015 08:15:14 GMT")
.build();
}
});
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(API_ROOT)
.setLogLevel(RestAdapter.LogLevel.HEADERS_AND_ARGS)
.setClient(new OkClient(client))
.setConverter(new SimpleXMLConverter(false))
.setRequestInterceptor(new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
if (Network.isConnected(context)) {
int maxAge = 60; // read from cache for 2 minutes
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);
}
}
})
.build();
api = restAdapter.create(ApiService.class);
当然,没有必要删除所有这些 headers,但我想让响应尽可能干净,以排除这些额外 headers 的一些干扰。
如您所见,我还尝试欺骗 Expires 和 Date header(我尝试删除它们,设置它们以便它们之间完全存在 max-age 差异并将 Expires 设置为远未来)。我还尝试了各种 Cache-control 值,但没有成功。
我确保 cacheFile 存在,isDirectory 并且可由应用程序写入。
这些是改造直接记录的请求和响应 header:
Request:
Cache-Control: public, max-age=60
---> END HTTP (no body)
Response:
Date: Mon, 27 Apr 2015 08:41:10 GMT
Server: Apache/2.2.22 (Ubuntu)
Expires: Mon, 27 Apr 2015 08:46:10 GMT
Content-Type: text/xml; charset=UTF-8
OkHttp-Selected-Protocol: http/1.1
OkHttp-Sent-Millis: 1430124070000
OkHttp-Received-Millis: 1430124070040
Cache-Control: public, max-age=60
<--- END HTTP (-1-byte body)
<--- BODY: ...
最后还有一件奇怪的事情:在某个时候,缓存工作了几分钟。我得到了合理的命中数,甚至离线请求也返回了缓存值。 (它发生在使用此处发布的确切设置时)但是当我重新启动应用程序时,一切都回到了 "normal"(持续命中计数 0)。
如果有人知道这里可能存在什么问题,我将非常乐意提供任何帮助:)
使用 networkInterceptors() 而不是 interceptors()。结合您删除任何与缓存相关的 headers 的策略将起作用。这是简短的回答。
当您使用拦截器更改 headers 时,它不会在调用 CacheStrategy.isCacheable() 之前进行任何调整。值得查看 CacheStrategy 和 CacheControl 类 以了解 OKHttp 如何处理 cache-related headers。在 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
上执行 ctrl+f "cache" 也是值得的我不确定 networkInterceptors() 和 interceptors() 文档是否不清楚或者是否存在错误。一旦我进一步研究,我会更新这个答案。
这里还有一件事要补充,除了 Brendan Weinstein 的回答只是为了确认 OkHttp3 缓存不适用于 post 请求。
一整天后,我发现我的离线缓存不起作用只是因为我在API类型中使用了POST
。我将其更改为 GET
的那一刻,它起作用了!
@GET("/ws/audioInactive.php")
Call<List<GetAudioEntity>> getAudios();
我的整个 Retrofit class。
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.limnet.iatia.App;
import com.limnet.iatia.netio.entity.registration.APIInterfaceProviderIMPL;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class RHTRetroClient {
public static final String BASE_URL = "https://abc.pro";
private static Retrofit retrofit = null;
private static RHTRetroClient mInstance;
private static final long cacheSize = 10 * 1024 * 1024; // 10 MB
public static final String HEADER_CACHE_CONTROL = "Cache-Control";
public static final String HEADER_PRAGMA = "Pragma";
private RHTRetroClient() {
Gson gson = new GsonBuilder()
.setLenient()
.create();
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
Cache cache = new Cache(new File(App.getAppContext().getCacheDir(), "soundbites"),cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(httpLoggingInterceptor()) // used if network off OR on
.addNetworkInterceptor(networkInterceptor()) // only used when network is on
.addInterceptor(offlineInterceptor())
.build();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
/**
* This interceptor will be called both if the network is available and if the network is not available
*
* @return
*/
private static Interceptor offlineInterceptor() {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Log.d("rht", "offline interceptor: called.");
Request request = chain.request();
// prevent caching when network is on. For that we use the "networkInterceptor"
if (!App.hasNetwork()) {
CacheControl cacheControl = new CacheControl.Builder()
.maxStale(7, TimeUnit.DAYS)
.build();
request = request.newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.cacheControl(cacheControl)
.build();
}
return chain.proceed(request);
}
};
}
/**
* This interceptor will be called ONLY if the network is available
*
* @return
*/
private static Interceptor networkInterceptor() {
return new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Log.d("rht", "network interceptor: called.");
Response response = chain.proceed(chain.request());
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(5, TimeUnit.SECONDS)
.build();
return response.newBuilder()
.removeHeader(HEADER_PRAGMA)
.removeHeader(HEADER_CACHE_CONTROL)
.header(HEADER_CACHE_CONTROL, cacheControl.toString())
.build();
}
};
}
private static HttpLoggingInterceptor httpLoggingInterceptor() {
HttpLoggingInterceptor httpLoggingInterceptor =
new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
Log.d("rht", "log: http log: " + message);
}
});
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return httpLoggingInterceptor;
}
public static synchronized RHTRetroClient getInstance() {
if (mInstance == null) {
mInstance = new RHTRetroClient();
}
return mInstance;
}
public APIInterfaceProviderIMPL getAPIInterfaceProvider() {
return retrofit.create(APIInterfaceProviderIMPL.class);
}
}
检查您的回复中是否有 Pragma header。如果 Pragma: no-cache
header 存在,使用 max-age
的缓存将不起作用。
如果确实有 Pragma
header,请在 Interceptor
中执行以下操作将其删除:
override fun intercept(chain: Interceptor.Chain): Response {
val cacheControl = CacheControl.Builder()
.maxAge(1, TimeUnit.MINUTES)
.build()
return originalResponse.newBuilder()
.header("Cache-Control", cacheControl.toString())
.removeHeader("Pragma") // Caching doesnt work if this header is not removed
.build()
}