如何修改Retrofit/OkHttp缓存满时的清理逻辑?
问题:自定义OkHttp缓存满时的淘汰策略
默认情况下,OkHttp(Retrofit基于OkHttp实现缓存)在缓存满时,会优先删除更早过期的缓存响应:
- 示例1:1MB空缓存中先存入700KB、1小时后过期(
Cache-Control: max-age=3600)的响应,再存入800KB、30分钟后过期的响应,后者无法被缓存。 - 示例2:1MB缓存中有800KB、1小时后过期的响应,存入800KB、5小时后过期的响应时,前者会被删除,后者被存储。
需求是修改为基于**接收时间(OkHttp-Received-Millis响应头)**删除缓存,即最早接收的响应优先被淘汰。
当前的Retrofit/OkHttp初始化代码:
private Retrofit retrofit(String baseUrl) { return new Retrofit.Builder() .baseUrl(baseUrl) .client(cachingClient(CACHE_IDENTIFIER)) .addConverterFactory(GsonConverterFactory.create(gson())) .build(); } private OkHttpClient cachingClient(String identifier) { return new OkHttpClient.Builder() .cache(cache(identifier)) .addInterceptor(InterceptorsKt.authorizationInterceptor(Credentials.basic(USER, PASSWD))) .build(); } private Cache cache(String identifier) { return new Cache(new File(context.get().getCacheDir(), identifier), CACHE_SIZE); }
缓存响应结构示例:
http://example.com:8754/random/hello GET 0 HTTP/1.1 200 11 Cache-Control: max-age=2592000, immutable X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block X-Frame-Options: DENY Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 21 Sep 2022 08:01:31 GMT Keep-Alive: timeout=60 Connection: keep-alive OkHttp-Sent-Millis: 1663747322078 OkHttp-Received-Millis: 1663747322254
解决方案
要实现基于接收时间的缓存淘汰,需要自定义Cache类,重写其缓存裁剪逻辑(trimToSize方法),替换默认的过期时间排序为OkHttp-Received-Millis排序。
1. 自定义Cache类
继承OkHttp的Cache类,重写trimToSize方法,修改缓存entry的排序规则:
import okhttp3.Cache; import okhttp3.Headers; import okhttp3.internal.cache.DiskLruCache; import okhttp3.internal.cache.DiskLruCache.Entry; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class ReceivedTimeBasedCache extends Cache { public ReceivedTimeBasedCache(File directory, long maxSize) { super(directory, maxSize); } @Override public synchronized void trimToSize(long maxSize) throws IOException { DiskLruCache cache = this.cache; if (cache == null) return; long currentSize = cache.size(); if (currentSize <= maxSize) return; // 收集所有缓存条目 List<Entry> entries = new ArrayList<>(); for (String key : cache.keys()) { Entry entry = cache.get(key); if (entry != null) { entries.add(entry); } } // 按OkHttp-Received-Millis从小到大排序,最早接收的先删除 Collections.sort(entries, (a, b) -> { try { Headers aHeaders = Headers.of(a.responseHeaders); Headers bHeaders = Headers.of(b.responseHeaders); long aReceivedTime = Long.parseLong(aHeaders.get("OkHttp-Received-Millis")); long bReceivedTime = Long.parseLong(bHeaders.get("OkHttp-Received-Millis")); return Long.compare(aReceivedTime, bReceivedTime); } catch (NumberFormatException | NullPointerException e) { // 解析失败时 fallback 到默认的过期时间排序 return Long.compare(a.expireAt, b.expireAt); } }); // 删除条目直到缓存大小符合要求 long bytesToRemove = currentSize - maxSize; for (Entry entry : entries) { if (bytesToRemove <= 0) break; cache.remove(entry.key); bytesToRemove -= entry.size; } } }
2. 替换原Cache初始化
修改cache方法,使用自定义的ReceivedTimeBasedCache:
private Cache cache(String identifier) { return new ReceivedTimeBasedCache(new File(context.get().getCacheDir(), identifier), CACHE_SIZE); }
注意事项
- 该实现依赖OkHttp内部的
DiskLruCache和Entry类(属于okhttp3.internal包),这些是内部API,不同OkHttp版本可能存在兼容性问题,升级时需要验证代码。 - 当
OkHttp-Received-Millis头解析失败时,会自动 fallback 到默认的过期时间排序,避免缓存裁剪逻辑异常。 - 重写的
trimToSize方法保持了父类的synchronized锁逻辑,确保线程安全。
内容的提问来源于stack exchange,提问作者MarekChr
相关产品推荐
相关产品推荐

