一日一学_okhttp(本地缓存)
在学习okhttp缓存策略之前,我先思考了web前端浏览器缓存的策略。
浏览器缓存(客户端缓存),它分为强缓存和协商缓存
- 强缓存
浏览器在加载资源时,先根据资源http header判断它是否命中强 缓存,强缓存如果命中,浏览器直接从自己的缓存中读取资源,不会发请求 到服务器。 - 协商缓存
当强缓存没有命中缓存时,浏览器一定会发送一个请求到服务器,通过服务器端依据资源的另外的http header验证这个资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回,但是不会返回这个资源的数据,而是告诉客户端可以直接从缓存中加载这个资源,于是浏览器就又会从自己的缓存中去加载这个资源;
强缓存
强缓存是利用Expires或者Cache-Control这两个http response header实现的,它们都用来表示资源在客户端缓存的有效期。
- Expires
Expires是http1.0提出的一个表示资源过期时间的header,它描述的是一个绝对时间,由服务器返回,用GMT格式的字符串表示,如:Expires:Thu, 31 Dec 2037 23:55:55 GMT,它的缓存原理是:
1 .浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Expires的header
- 浏览器在接收到这个资源后,会把这个资源连同所有response header一起缓存下来
- 浏览器再次请求这个资源时,先从缓存中寻找,找到这个资源后,拿出它的Expires跟当前的请求时间比较,如果请求时间在Expires指定的时间之前,就能命中缓存,否则就不行。
- 如果缓存没有命中,浏览器直接从服务器加载资源时,Expires Header在重新加载的时候会被更新。
Expires是较老的强缓存管理header,由于它是服务器返回的一个绝对时间,在服务器时间与客户端时间相差较大时,缓存管理容易出现问题,比如随意修改下客户端时间,就能影响缓存命中的结果。所以在http1.1的时候,提出了一个新的header,就是Cache-Control,这是一个相对时间,在配置缓存的时候,以秒为单位,用数值表示,如:Cache-Control:max-age=315360000,它的缓存原理与Expires相似。
- Cache-Control与Expires不同之处
Cache-Control描述的是一个相对时间,在进行缓存命中的时候,都是利用客户端时间进行判断,所以相比较Expires,Cache-Control的缓存管理更有效,安全一些。这两个header可以只启用一个,也可以同时启用,当response header中,Expires和Cache-Control同时存在时,Cache-Control优先级高于Expires:
协商缓存
协商缓存跟强缓存不一样,强缓存不发请求到服务器,所以有时候资源更新了都在本地,但是协商缓存会发请求到服务器,所以资源是否更新,服务器肯定知道。大部分web服务器都默认开启协商缓存,而且是同时启用(Last-Modified,If-Modified-Since) 和 (ETag、If-None-Match)。
- Last-Modified,If-Modified-Since
- 浏览器第一次跟服务器请求资源时,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间。
- 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值。
- 服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since与服务器上的最后修改时间判断,如果没有变化则返回304 Not Modified(不会返回资源内容,header也不会改变);如果有变化,就正常返回资源内容。
- 浏览器收到304的响应后,就会从缓存中加载资源。(没有命中,浏览器直接从服务器加载资源,Header在重新加载更新)
(Last-Modified,If-Modified-Since)根据服务器时间返回的header,一般来说,在没有调整服务器时间和篡改客户端缓存的情况下,这两个header配合是非常可靠的,但是有时候也会服务器上资源其实有变化,但是最后修改时间却没有变化的情况,而这种问题又很不容易被定位出来,而当这种情况出现的时候,就会影响协商缓存的可靠性。所以就有了另外一对header来管理协商缓存,这对header就是(ETag、If-None-Match)。
- ETag、If-None-Match
- 览器第一次跟服务器请求资源,服务器在返回这个资源的同时,在respone的header加上ETag的header,这个header是服务器根据当前请求的资源生成的一个唯一标识,这个唯一标识是一个字符串,只要资源有变化这个串就不同,跟最后修改时间没有关系.
- 浏览器再次跟服务器请求这个资源时,在request的header上加上If-None-Match的header,这个header的值就是上一次请求时返回的ETag的值.
- 服务器再次收到资源请求时,根据浏览器传过来If-None-Match和然后再根据资源生成一个新的ETag,如果这两个值相同就说明资源没有变化,否则就是有变化;如果没有变化则返回304 Not Modified,如果有变化,就正常返回资源内容。与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化.
- 浏览器收到304的响应后,就会从缓存中加载资源。
上面为http简单缓存知识。接下来我们来查看okhttp缓存策略(和浏览器原理差不多,要不总结这么多白瞎了)。
okHttp源码分析
OkHttp中缓存策略与浏览器处理大同小异。
我们只看CacheStrategy的getCandidate()方法
private CacheStrategy getCandidate() {
//如果缓存没有命中,就不需要加缓存Header了
if (cacheResponse == null) {
//没有缓存的网络请求,直接访问
return new CacheStrategy(request, null);
}
// 如果缓存的TLS握手信息丢失,返回进行直接连接
if (request.isHttps() && cacheResponse.handshake() == null) {
//直接访问
return new CacheStrategy(request, null);
}
//检测response的状态码,Expired时间,是否有no-cache标签
if (!isCacheable(cacheResponse, request)) {
//直接访问
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
//有ETag/Since标签
if (requestCaching.noCache() || hasConditions(request)) {
//直接连接,把缓存判断交给服务器
return new CacheStrategy(request, null);
}
//根据RFC协议计算
long ageMillis = cacheResponseAge();
//max-age
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//max-age
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
//大部分情况下设置是0
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
//ParseHeader中的缓存控制信息
CacheControl responseCaching = cacheResponse.cacheControl();
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
//设置最大过期时间,一般设置为0
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//缓存在过期时间内,可以使用
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
//返回上次的缓存
Response.Builder builder = cacheResponse.newBuilder();
return new CacheStrategy(null, builder.build());
}
//缓存失效, 如果有etag等信息
//发送请求,交给服务器处理
Request.Builder conditionalRequestBuilder = request.newBuilder();
if (etag != null) {
conditionalRequestBuilder.header("If-None-Match", etag);
} else if (lastModified != null) {
conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
} else if (servedDate != null) {
conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
}
//网络请求
Request conditionalRequest = conditionalRequestBuilder.build();
return hasConditions(conditionalRequest) ? new CacheStrategy(conditionalRequest,
cacheResponse) : new CacheStrategy(conditionalRequest, null);
}
okhttp源码可以看出缓存完全由服务器Header决定的,自己没有必要进行控制。用Interceptor中手工添加缓存代码控制,在实时换取更换数据的时候,会出现使用缓存数据的风险(自己项目出现过bug)。
嘿嘿嘿,前面都是铺垫和我遇到的坑,接下来才是本文正文:
使用Rxjava进行缓存
先对RxCache 对象进行数据的初始化
public final class RxCache {
//缓存是基于LruCache,DiskLruCache上进行的,这一步是初始化这俩个
//主角,后面会进行讲解。
private RxCache(int memoryMaxSize, int appVersion, long diskMaxSize, File diskDir, IDiskConverter diskConverter) {
cacheCore = new CacheCore(new LruMemoryCache(memoryMaxSize), new LruDiskCache(diskConverter,diskDir,appVersion,diskMaxSize));
}
public static final class Builder {
private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
private static final int DEFAULT_MEMORY_CACHE_SIZE=(int) (Runtime.getRuntime().maxMemory()/8);//运行内存的8分之1
private int memoryMaxSize;
private int appVersion;
private long diskMaxSize;
private File diskDir;
private IDiskConverter diskConverter;
public Builder(){
}
/**
* 不设置,默认为运行内存的8分之1
*/
public Builder memorySize(int maxSize) {
this.memoryMaxSize = maxSize;
return this;
}
/**
* 不设置,默认为1
*/
public Builder appVersion(int appVersion) {
this.appVersion = appVersion;
return this;
}
public Builder diskDir(File directory) {
this.diskDir = directory;
return this;
}
public Builder diskConverter(IDiskConverter converter) {
this.diskConverter = converter;
return this;
}
/**
* 不设置, 默为认50MB
*/
public Builder diskSize(long maxSize) {
this.diskMaxSize = maxSize;
return this;
}
public RxCache build() {
if(this.diskDir==null){
throw new NullPointerException("DiskDir can not be null");
}
if (!this.diskDir.exists()) {
this.diskDir.mkdirs();
}
if(this.diskConverter==null){
this.diskConverter=new DiskConverter();
}
if(memoryMaxSize<=0){
memoryMaxSize= DEFAULT_MEMORY_CACHE_SIZE;
}
if(diskMaxSize<=0){
diskMaxSize=MAX_DISK_CACHE_SIZE;
}
appVersion= Math.max(1,this.appVersion);
//初始化
return new RxCache(memoryMaxSize,appVersion,diskMaxSize,diskDir,diskConverter);
}
}
}
也可以自己进行配置
//前面我讲过Builder模式,这里就可以体会到了
public static RxCache getRxCache(Context context) {
RxCache rxCache = new RxCache.Builder()
.diskDir(new File(context.getCacheDir().getPath() + File.separator + "data"))
.diskConverter(new DiskConverter())
.memorySize(2*1024*1024)
.build();
return rxCache;
}
DiskConverter这个转换类,为了以后扩展你可以存到本地或者数据库。
public class DiskConverter implements IDiskConverter {
@Override
public Object load(InputStream source) {
Object value = null;
ObjectInputStream oin = null;
try {
oin = new ObjectInputStream(source);
value = oin.readObject();
} catch (IOException | ClassNotFoundException e) {
} finally {
close(oin);
}
return value;
}
@Override
public boolean writer(OutputStream sink, Object data) {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(sink);
oos.writeObject(data);
oos.flush();
return true;
} catch (IOException e) {
return false;
} finally {
close(oos);
}
}
public void close(Closeable close) {
if (close != null) {
try {
closeThrowException(close);
} catch (IOException ignored) {
}
}
}
public void closeThrowException(Closeable close) throws IOException {
if (close != null) {
close.close();
}
}
}
加载网络网址事例:
RxNetwork.getInstance()
.createApi(Api.class, false)
.getAd(1, 2)
//主要这一部分,缓存的核心一步
.compose(rxCache.<AdBean>transformer("cache", CacheProviders.cache))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new RxSubscriber<AdBean>() {
@Override
public void onSuccess(AdBean adBeanResult) {
Log.i("MainActivity", adBeanResult.getData().getAdList().get(0).getPicUrl());
}
@Override
public void onFailed(Throwable e) {
}
});
讲之前我们重新认识Rxjava的Transformer这个老朋友。
Transformer代码是Func1<Observable<T>, Observable<R>>,换言之就是:可以通过它将一种类型的Observable转换成另一种类型的Observable。呸,这是什么鬼,每篇博客都这么写,demo还不给,逗我玩。
我给大家留下demo,自己琢磨去吧
//运行....
main(){
Observable.just("1","2").compose(RxDemoTransformer.<String>transformerTest()).subscribe(new Subscriber<Integer>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(Integer integer) {
Log.i("MainActivity",integer.intValue()+"");
}
});
}
-------------------------------
public class RxDemoTransformer {
public static <String>Observable.Transformer<String,Integer> transformerTest(){
return new Observable.Transformer<String,Integer>(){
@Override
public Observable<Integer> call(Observable<String> stringObservable) {
return stringObservable.map(new Func1<String, Integer>() {
@Override
public Integer call(String string) {
return Integer.decode((java.lang.String) string);
}
});
}
};
}
}
运行结果了,本人懒自己运行去吧。
//通过key进行数据的内存缓存和本地缓存,CacheProviders 是控制网络和本地的终于类
public <T> Observable.Transformer<T, Result<T>> transformer(final
String key, final CacheProviders providers) {
return new Observable.Transformer<T, Result<T>>() {
@Override
public Observable<Result<T>> call(Observable<T> tObservable) {
return providers.execute(RxCache.this,key,tObservable);
}
};
}
来看看CacheProviders这个如何进行网络,缓存切换的(还存在bug,大家慢慢找,呵呵。。。)
public final class CacheProviders {
public static final CacheProviders cache=new CacheProviders();
public <T> Observable<Result<T>> execute(RxCache rxCache, String key, Observable<T> source) {
Observable<Result<T>> cache = loadCache(rxCache,key);
Observable<Result<T>> remote = loadRemote(rxCache,key, source, CacheType.MemoryAndDisk)
.onErrorReturn(new Func1<Throwable, Result<T>>() {
@Override
public Result<T> call(Throwable throwable) {
return null;
}
});
return Observable.concat(remote, cache)
.firstOrDefault(null, new Func1<Result<T>, Boolean>() {
@Override
public Boolean call(Result<T> tResultData) {
return tResultData != null && tResultData.data != null;
}
});
}
//加载缓存数据
<T> Observable<Result<T>> loadCache(final RxCache rxCache, final String key) {
return rxCache
.<T>load(key)
.map(new Func1<T, Result<T>>() {
@Override
public Result<T> call(T o) {
return new Result<>(ResultFrom.Cache, key, o);
}
});
}
//加载网络数据
<T> Observable<Result<T>> loadRemote(final RxCache rxCache, final String key, Observable<T> source, final CacheType target) {
return source
.map(new Func1<T, Result<T>>() {
@Override
public Result<T> call(T t) {
//保存网络数据
rxCache.save(key, t,target).subscribeOn(Schedulers.io())
.subscribe(new Action1<Boolean>() {
@Override
public void call(Boolean status) {
}
});
return new Result<>(ResultFrom.Remote, key, t);
}
});
}
}
OnErrorReturn是什么鬼。
OnErrorReturn-当发生错误的时候,让Observable发射一个预先定义好的数据并正常地终止
onErrorReturn
举上面的例子,当loadRemote没有网络就会报错,立马会执行onErrorReturn返回一个null。
concat()操作符持有多个Observable对象,并将它们按顺序串联成队列。
firstOrDefault() 阻塞直到Observable发射了一个数据或者终止,返回第一项数据,或者返回默认值.又是一些概念,让老夫撸一串代码,就明白了。
Observable<String> oba =Observable.just("1");
Observable<String> obb =Observable.just("2");
Observable.concat(oba, obb).firstOrDefault(null, new Func1<String, Boolean>() {
@Override
public Boolean call(String s) {
if (s.equals("2")){
return true;
}
return false;
}
}).subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(String s) {
Log.i("MainActivity","MainActivity --->"+s);
}
});
运行结果
MainActivity: MainActivity --->2
通过demo是不是我上面的缓存代码明白是什么策略了。
//缓存核心,LruMemoryCache 对lruCache封装,进行保存与读取
//LruDiskCache DiskLruCache进行了封装,进行保存与读取
class CacheCore {
private LruMemoryCache memory;
private LruDiskCache disk;
CacheCore(LruMemoryCache memory, LruDiskCache disk) {
this.memory = memory;
this.disk = disk;
}
/**
* 读取
*/
<T> T load(String key) {
if (memory != null) {
T result = memory.load(key);
if (result != null) {
return result;
}
}
if (disk != null) {
T result = disk.load(key);
if (result != null) {
return result;
}
}
return null;
}
/**
* 保存
*/
<T> boolean save(String key, T value, CacheType target) {
if (value == null) { //如果要保存的值为空,则删除
return memory.remove(key) && disk.remove(key);
}
if (target.supportMemory() && memory != null) {
memory.save(key, value);
}
if (target.supportDisk() && disk != null) {
return disk.save(key, value);
}
return false;
}
}
LruMemoryCache 存储到内存中,下次加载页面更快加载,提高用户体验.
class LruMemoryCache {
//lruCache算法是最近最少使用算法(LinkedHashMap封装)。
//我会新写一篇解释lruCache的实现
private LruCache<String, Serializable> mCache;
private final HashSet<String> mKeySet;
public LruMemoryCache(final int cacheSize) {
mKeySet = new HashSet<>();
mCache = new LruCache<String, Serializable>(cacheSize) {
@Override
protected int sizeOf(String key, Serializable value) {
return calcSize(value);
}
};
}
//现在明白,当获取数据的时候,数据会排到LinkedHashMap队尾就可以
public <T> T load(String key) {
return (T) mCache.get(key);
}
public <T> boolean save(String key, T value) {
if (null != value) {
mCache.put(key, (Serializable) value);
mKeySet.add(key);
}
return true;
}
..........
}
LruDiskCache存储到本地核心类(DiskLruCache 我会新写一篇进行讲解)
class LruDiskCache {
private IDiskConverter mDiskConverter;
private DiskLruCache mDiskLruCache;
LruDiskCache(IDiskConverter diskConverter, File diskDir, int appVersion, long diskMaxSize) {
this.mDiskConverter = diskConverter;
try {
mDiskLruCache = DiskLruCache.open(diskDir, appVersion, 1, diskMaxSize);
} catch (IOException e) {
e.printStackTrace();
}
}
<T> T load(String key) {
if (mDiskLruCache == null) {
return null;
}
try {
DiskLruCache.Editor edit = mDiskLruCache.edit(key);
if (edit == null) {
return null;
}
InputStream source = edit.newInputStream(0);
T value ;
if (source != null) {
value = (T) mDiskConverter.load(source);
close(source);
edit.commit();
return value;
}
edit.abort();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
<T> boolean save(String key, T value) {
if (mDiskLruCache == null) {
return false;
}
//如果要保存的值为空,则删除
if (value == null) {
return remove(key);
}
try {
DiskLruCache.Editor edit = mDiskLruCache.edit(key);
if (edit == null) {
return false;
}
OutputStream sink = edit.newOutputStream(0);
if (sink != null) {
mDiskConverter.writer(sink, value);
close(sink);
edit.commit();
return true;
}
edit.abort();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
....................
}