前言
按照HTTP缓存机制和REST API设计规范,我们不应该缓存POST请求结果, 所以Okhttp官方也没有实现对POST请求结果进行缓存,以下是Okhttp源码注释
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
You can't cache POST requests with OkHttp’s cache. You’ll need to store them using some other mechanism
但是现实社会很残酷,由于各种原因, 我们身边有很多用 POST请求当作GET使用请求的API. 对于这种情况,我们就要自己实现POST请求缓存链了.
本文将讲述Okhttp3+Retrofit实现POST请求缓存链过程, 当然也可以用于GET请求,但是不建议那么做,因为Okhttp对缓存GET请求支持的很完美.
特点
如果缓存有数据,并且数据没有过期,那么直接取缓存数据;
如果缓存过期,则直接从网络获取数据;
支持直接从缓存中读数据
支持忽略缓存,直接从网络获取数据
支持自由精确地配置缓存有效时间
内存缓存
很简单,直接封装android.support.v4.util.LruCache类, 外加上过期时间判断即可
public class MemoryCache {
private final LruCache cache;
private final List keys = new ArrayList<>();
public MemoryCache(int maxSize) {
this.cache = new LruCache<>(maxSize);
}
private void lookupExpired() {
Completable.fromAction(
() -> {
String key;
for (int i = 0; i < keys.size(); i++) {
key = keys.get(i);
Entry value = cache.get(key);
if (value != null && value.isExpired()) {
remove(key);
}
}
})
.subscribeOn(Schedulers.single())
.subscribe();
}
@CheckForNull
public synchronized Entry get(String key) {
Entry value = cache.get(key);
if (value != null && value.isExpired()) {
remove(key);
lookupExpired();
return null;
}
lookupExpired();
return value;
}
public synchronized Entry put(String key, Entry value) {
if (!keys.contains(key)) {
keys.add(key);
}
Entry oldValue = cache.put(key, value);
lookupExpired();
return oldValue;
}
public Entry remove(String key) {
keys.remove(key);
return cache.remove(key);
}
public Map snapshot() {
return cache.snapshot();
}
public void trimToSize(int maxSize) {
cache.trimToSize(maxSize);
}
public int createCount() {
return cache.createCount();
}
public void evictAll() {
cache.evictAll();
}
public int evictionCount() {
return cache.evictionCount();
}
public int hitCount() {
return cache.hitCount();
}
public int maxSize() {
return cache.maxSize();
}
public int missCount() {
return cache.missCount();
}
public int putCount() {
return cache.putCount();
}
public int size() {
return cache.size();
}
@Immutable
public static final class Entry {
@SerializedName("data")
public final Object data;
@SerializedName("ttl")
public final long ttl;
}
}
硬盘缓存
同样很简单,直接参照Okhttp的Cache类逻辑, 直接封装DiskLruCache类, 外加上过期时间判断即可.
public final class DiskCache implements Closeable, Flushable {
/**
* Unlike {@link okhttp3.Cache} ENTRY_COUNT = 2
* We don't save the CacheHeader and Respond in two separate files
* Instead, we wrap them in {@link Entry}
*/
private static final int ENTRY_COUNT = 1;
private static final int VERSION = 201105;
private static final int ENTRY_METADATA = 0;
private final DiskLruCache cache;
public DiskCache(File directory, long maxSize) {
cache = DiskLruCache.create(FileSystem.SYSTEM, directory, VERSION, ENTRY_COUNT, maxSize);
}
public Entry get(String key) {
DiskLruCache.Snapshot snapshot;
try {
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
return null;
}
try {
BufferedSource source = Okio.buffer(snapshot.getSource(0));
String json = source.readUtf8();
source.close();
Util.closeQuietly(snapshot);
return DataLayerUtil.fromJson(json, null, Entry.class);
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
}
public void put(String key, Entry entry) {
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key);
if (editor != null) {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(entry.toString());//Entry.toString() is json String
sink.close();
editor.commit();
}
} catch (IOException e) {
abortQuietly(editor);
}
}
public void remove(String key) throws IOException {
cache.remove(key);
}
private void abortQuietly(DiskLruCache.Editor editor) {
try {
if (editor != null) {
editor.abort();
}
} catch (IOException ignored) {
}
}
public void initialize() throws IOException {
cache.initialize();
}
public void delete() throws IOException {
cache.delete();
}
public void evictAll() throws IOException {
cache.evictAll();
}
public long size() throws IOException {
return cache.size();
}
public long maxSize() {
return cache.getMaxSize();
}
public File directory() {
return cache.getDirectory();
}
public boolean isClosed() {
return cache.isClosed();
}
@Override
public void flush() throws IOException {
cache.flush();
}
@Override
public void close() throws IOException {
cache.close();
}
/**
* Data and metadata for an entry returned by the cache.
* It's extracted from android Volley library.
* See {@code https://github.com/google/volley}
*/
@Immutable
public static final class Entry {
/**
* The data returned from cache.
* Use {@link com.thepacific.data.common.DataLayerUtil#toJsonByteArray(Object, Gson)}
* to serialize a data object
*/
@SerializedName("data")
public final byte[] data;
/**
* Time to live(TTL) for this record
*/
@SerializedName("ttl")
public final long ttl;
/**
* Soft TTL for this record
*/
@SerializedName("softTtl")
public final long softTtl;
/**
* @return To a json String
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{")
.append("data=")
.append(Arrays.toString(data))
.append(", ttl=")
.append(ttl)
.append(", softTtl=")
.append(softTtl)
.append("}");
return builder.toString();
}
/**
* True if the entry is expired.
*/
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/**
* True if a refresh is needed from the original data source.
*/
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
实现Repository
写一个Repository
/**
* A repository can get cached data {@link Repository#get(Object)}, or force
* a call to network(skipping cache) {@link Repository#fetch(Object, boolean)}
*/
public abstract class Repository {
protected final Gson gson;
protected final DiskCache diskCache;
protected final MemoryCache memoryCache;
protected final OnAccessFailure onAccessFailure;
protected String key;
public Repository(Gson gson,
DiskCache diskCache,
MemoryCache memoryCache,
OnAccessFailure onAccessFailure) {
this.gson = gson;
this.diskCache = diskCache;
this.memoryCache = memoryCache;
this.onAccessFailure = onAccessFailure;
}
/**
* Return an Observable of {@link Source } for request query
* Data will be returned from oldest non expired source
* Sources are memory cache, disk cache, finally network
*/
@Nonnull
public final Observable
使用
@Test
public void testGet() {
userRepo.get(userQuery)
.onErrorReturn(e -> Source.failure(e))
.startWith(Source.inProgress())
.subscribe(it -> {
switch (it.status) {
case IN_PROGRESS:
System.out.println("Show Loading Dialog===============");
break;
case IRRELEVANT:
System.out.println("Empty Data===============");
break;
case ERROR:
System.out.println("Error Occur===============");
break;
case SUCCESS:
System.out.println("Update UI===============");
break;
default:
throw new UnsupportedOperationException();
}
});
assertEquals(2, userRepo.memory().size());
}
源码
完整源码请到点击,并查看data模块,具体使用请参照单元测试代码
此外,因为时间原因,现状态的源码属于雏形阶段的代码,代码多处地方存在不合理或者错误. 09月05日前会把生产线上的代码完整后上传到github