Retrofit + OkHTTP - 响应缓存不起作用

6

我知道有很多类似的问题,但是我已经阅读了所有回答,没有一个能够真正帮到我。

我的问题如下:

我正在使用retrofit + okhttp从API获取一些数据,并且想要进行缓存。不幸的是,我没有管理员权限去修改API服务器返回的头部信息。(目前,服务器返回Cache-control: private)

因此,我决定使用okhttp头部欺骗来插入适当的缓存头部。不幸的是,无论我做什么,缓存似乎都无法生效。

我像这样初始化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);

当然,没有必要删除所有这些头文件,但我想尽可能使响应看起来更加干净,以排除一些来自这些额外头文件的干扰。
如您所见,我尝试了伪造Expires和Date标头(我尝试删除它们,将它们设置为恰好与它们之间的最大年龄差异,并将Expires设置为远期)。 我还尝试了各种Cache-control值,但没有成功。
我确保cacheFile存在,是应用程序可写的目录。
这些是由retrofit直接记录的请求和响应头文件:
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: ...

最后,发生了一件奇怪的事情:某个时刻,缓存工作了几分钟。我得到了合理的命中次数,即使是离线请求也返回了缓存值。(发生在使用此处发布的确切设置时) 但是当我重新启动应用程序时,一切都恢复到“正常状态”(命中计数恒为0)。

如果有人知道这里可能出现什么问题,我会非常感激任何帮助 :)

4个回答

10
请使用networkInterceptors()代替interceptors()。结合删除与缓存有关的任何标头的策略,这将起作用。那就是简短的答案。
当您使用拦截器更改标头时,在CacheStrategy.isCacheable()被调用之前不会进行任何调整。值得查看CacheStrategy和CacheControl类,以了解OKHttp如何处理与缓存相关的标头。还值得在http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html上执行ctrl+f“cache”。
我不确定networkInterceptors()和interceptors()文档是否不清楚或是否存在错误。一旦我更深入地研究它,我会更新这个答案。

1
哦,天啊,我真是太蠢了。非常感谢你。我一定是有某种时间盲点或者其他问题——我从来没有意识到那段代码中有interceptors()而不是networkInterceptors()... - daemontus

8

在这里还有一件事情需要补充,除了Brendan Weinstein的回答之外,需要确认的是OkHttp3缓存不适用于POST请求。


1
在整整一天之后,我发现我的离线缓存没有工作,只是因为我在API类型中使用了POST。当我把它改成GET时,它就可以工作了!
@GET("/ws/audioInactive.php")
Call<List<GetAudioEntity>> getAudios();

我的整个Retrofit类。
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);
    }

}

0

检查响应中是否有Pragma头。如果存在Pragma: no-cache头,则使用max-age进行缓存将无法正常工作。

如果确实有Pragma头,请通过在您的拦截器中执行以下操作来删除它:

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()
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接