在學(xué)習(xí)okhttp緩存策略之前,我先思考了web前端瀏覽器緩存的策略。
瀏覽器緩存(客戶端緩存),它分為強緩存和協(xié)商緩存
- 強緩存
瀏覽器在加載資源時,先根據(jù)資源http header判斷它是否命中強 緩存,強緩存如果命中,瀏覽器直接從自己的緩存中讀取資源,不會發(fā)請求 到服務(wù)器。 - 協(xié)商緩存
當(dāng)強緩存沒有命中緩存時,瀏覽器一定會發(fā)送一個請求到服務(wù)器,通過服務(wù)器端依據(jù)資源的另外的http header驗證這個資源是否命中協(xié)商緩存,如果協(xié)商緩存命中,服務(wù)器會將這個請求返回,但是不會返回這個資源的數(shù)據(jù),而是告訴客戶端可以直接從緩存中加載這個資源,于是瀏覽器就又會從自己的緩存中去加載這個資源;
強緩存
強緩存是利用Expires或者Cache-Control這兩個http response header實現(xiàn)的,它們都用來表示資源在客戶端緩存的有效期。
- Expires
Expires是http1.0提出的一個表示資源過期時間的header,它描述的是一個絕對時間,由服務(wù)器返回,用GMT格式的字符串表示,如:Expires:Thu, 31 Dec 2037 23:55:55 GMT,它的緩存原理是:
1 .瀏覽器第一次跟服務(wù)器請求一個資源,服務(wù)器在返回這個資源的同時,在respone的header加上Expires的header
- 瀏覽器在接收到這個資源后,會把這個資源連同所有response header一起緩存下來
- 瀏覽器再次請求這個資源時,先從緩存中尋找,找到這個資源后,拿出它的Expires跟當(dāng)前的請求時間比較,如果請求時間在Expires指定的時間之前,就能命中緩存,否則就不行。
- 如果緩存沒有命中,瀏覽器直接從服務(wù)器加載資源時,Expires Header在重新加載的時候會被更新。
Expires是較老的強緩存管理header,由于它是服務(wù)器返回的一個絕對時間,在服務(wù)器時間與客戶端時間相差較大時,緩存管理容易出現(xiàn)問題,比如隨意修改下客戶端時間,就能影響緩存命中的結(jié)果。所以在http1.1的時候,提出了一個新的header,就是Cache-Control,這是一個相對時間,在配置緩存的時候,以秒為單位,用數(shù)值表示,如:Cache-Control:max-age=315360000,它的緩存原理與Expires相似。
- Cache-Control與Expires不同之處
Cache-Control描述的是一個相對時間,在進行緩存命中的時候,都是利用客戶端時間進行判斷,所以相比較Expires,Cache-Control的緩存管理更有效,安全一些。這兩個header可以只啟用一個,也可以同時啟用,當(dāng)response header中,Expires和Cache-Control同時存在時,Cache-Control優(yōu)先級高于Expires:
協(xié)商緩存
協(xié)商緩存跟強緩存不一樣,強緩存不發(fā)請求到服務(wù)器,所以有時候資源更新了都在本地,但是協(xié)商緩存會發(fā)請求到服務(wù)器,所以資源是否更新,服務(wù)器肯定知道。大部分web服務(wù)器都默認開啟協(xié)商緩存,而且是同時啟用(Last-Modified,If-Modified-Since) 和 (ETag、If-None-Match)。
- Last-Modified,If-Modified-Since
- 瀏覽器第一次跟服務(wù)器請求資源時,服務(wù)器在返回這個資源的同時,在respone的header加上Last-Modified的header,這個header表示這個資源在服務(wù)器上的最后修改時間。
- 瀏覽器再次跟服務(wù)器請求這個資源時,在request的header上加上If-Modified-Since的header,這個header的值就是上一次請求時返回的Last-Modified的值。
- 服務(wù)器再次收到資源請求時,根據(jù)瀏覽器傳過來If-Modified-Since與服務(wù)器上的最后修改時間判斷,如果沒有變化則返回304 Not Modified(不會返回資源內(nèi)容,header也不會改變);如果有變化,就正常返回資源內(nèi)容。
- 瀏覽器收到304的響應(yīng)后,就會從緩存中加載資源。(沒有命中,瀏覽器直接從服務(wù)器加載資源,Header在重新加載更新)
(Last-Modified,If-Modified-Since)根據(jù)服務(wù)器時間返回的header,一般來說,在沒有調(diào)整服務(wù)器時間和篡改客戶端緩存的情況下,這兩個header配合是非常可靠的,但是有時候也會服務(wù)器上資源其實有變化,但是最后修改時間卻沒有變化的情況,而這種問題又很不容易被定位出來,而當(dāng)這種情況出現(xiàn)的時候,就會影響協(xié)商緩存的可靠性。所以就有了另外一對header來管理協(xié)商緩存,這對header就是(ETag、If-None-Match)。
- ETag、If-None-Match
- 覽器第一次跟服務(wù)器請求資源,服務(wù)器在返回這個資源的同時,在respone的header加上ETag的header,這個header是服務(wù)器根據(jù)當(dāng)前請求的資源生成的一個唯一標識,這個唯一標識是一個字符串,只要資源有變化這個串就不同,跟最后修改時間沒有關(guān)系.
- 瀏覽器再次跟服務(wù)器請求這個資源時,在request的header上加上If-None-Match的header,這個header的值就是上一次請求時返回的ETag的值.
- 服務(wù)器再次收到資源請求時,根據(jù)瀏覽器傳過來If-None-Match和然后再根據(jù)資源生成一個新的ETag,如果這兩個值相同就說明資源沒有變化,否則就是有變化;如果沒有變化則返回304 Not Modified,如果有變化,就正常返回資源內(nèi)容。與Last-Modified不一樣的是,當(dāng)服務(wù)器返回304 Not Modified的響應(yīng)時,由于ETag重新生成過,response header中還會把這個ETag返回,即使這個ETag跟之前的沒有變化.
- 瀏覽器收到304的響應(yīng)后,就會從緩存中加載資源。
上面為http簡單緩存知識。接下來我們來查看okhttp緩存策略(和瀏覽器原理差不多,要不總結(jié)這么多白瞎了)。
okHttp源碼分析
OkHttp中緩存策略與瀏覽器處理大同小異。
我們只看CacheStrategy的getCandidate()方法
private CacheStrategy getCandidate() {
//如果緩存沒有命中,就不需要加緩存Header了
if (cacheResponse == null) {
//沒有緩存的網(wǎng)絡(luò)請求,直接訪問
return new CacheStrategy(request, null);
}
// 如果緩存的TLS握手信息丟失,返回進行直接連接
if (request.isHttps() && cacheResponse.handshake() == null) {
//直接訪問
return new CacheStrategy(request, null);
}
//檢測response的狀態(tài)碼,Expired時間,是否有no-cache標簽
if (!isCacheable(cacheResponse, request)) {
//直接訪問
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
//有ETag/Since標簽
if (requestCaching.noCache() || hasConditions(request)) {
//直接連接,把緩存判斷交給服務(wù)器
return new CacheStrategy(request, null);
}
//根據(jù)RFC協(xié)議計算
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) {
//大部分情況下設(shè)置是0
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
//ParseHeader中的緩存控制信息
CacheControl responseCaching = cacheResponse.cacheControl();
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
//設(shè)置最大過期時間,一般設(shè)置為0
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//緩存在過期時間內(nèi),可以使用
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
//返回上次的緩存
Response.Builder builder = cacheResponse.newBuilder();
return new CacheStrategy(null, builder.build());
}
//緩存失效, 如果有etag等信息
//發(fā)送請求,交給服務(wù)器處理
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);
}
//網(wǎng)絡(luò)請求
Request conditionalRequest = conditionalRequestBuilder.build();
return hasConditions(conditionalRequest) ? new CacheStrategy(conditionalRequest,
cacheResponse) : new CacheStrategy(conditionalRequest, null);
}
okhttp源碼可以看出緩存完全由服務(wù)器Header決定的,自己沒有必要進行控制。用Interceptor中手工添加緩存代碼控制,在實時換取更換數(shù)據(jù)的時候,會出現(xiàn)使用緩存數(shù)據(jù)的風(fēng)險(自己項目出現(xiàn)過bug)。
嘿嘿嘿,前面都是鋪墊和我遇到的坑,接下來才是本文正文:
使用Rxjava進行緩存
先對RxCache 對象進行數(shù)據(jù)的初始化
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);//運行內(nèi)存的8分之1
private int memoryMaxSize;
private int appVersion;
private long diskMaxSize;
private File diskDir;
private IDiskConverter diskConverter;
public Builder(){
}
/**
* 不設(shè)置,默認為運行內(nèi)存的8分之1
*/
public Builder memorySize(int maxSize) {
this.memoryMaxSize = maxSize;
return this;
}
/**
* 不設(shè)置,默認為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;
}
/**
* 不設(shè)置, 默為認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這個轉(zhuǎn)換類,為了以后擴展你可以存到本地或者數(shù)據(jù)庫。
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();
}
}
}
加載網(wǎng)絡(luò)網(wǎng)址事例:
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轉(zhuǎn)換成另一種類型的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);
}
});
}
};
}
}
運行結(jié)果了,本人懶自己運行去吧。
//通過key進行數(shù)據(jù)的內(nèi)存緩存和本地緩存,CacheProviders 是控制網(wǎng)絡(luò)和本地的終于類
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這個如何進行網(wǎng)絡(luò),緩存切換的(還存在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;
}
});
}
//加載緩存數(shù)據(jù)
<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);
}
});
}
//加載網(wǎng)絡(luò)數(shù)據(jù)
<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) {
//保存網(wǎng)絡(luò)數(shù)據(jù)
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-當(dāng)發(fā)生錯誤的時候,讓Observable發(fā)射一個預(yù)先定義好的數(shù)據(jù)并正常地終止
舉上面的例子,當(dāng)loadRemote沒有網(wǎng)絡(luò)就會報錯,立馬會執(zhí)行onErrorReturn返回一個null。
concat()操作符持有多個Observable對象,并將它們按順序串聯(lián)成隊列。
firstOrDefault() 阻塞直到Observable發(fā)射了一個數(shù)據(jù)或者終止,返回第一項數(shù)據(jù),或者返回默認值.又是一些概念,讓老夫擼一串代碼,就明白了。
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);
}
});
運行結(jié)果
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 存儲到內(nèi)存中,下次加載頁面更快加載,提高用戶體驗.
class LruMemoryCache {
//lruCache算法是最近最少使用算法(LinkedHashMap封裝)。
//我會新寫一篇解釋lruCache的實現(xiàn)
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);
}
};
}
//現(xiàn)在明白,當(dāng)獲取數(shù)據(jù)的時候,數(shù)據(jù)會排到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;
}
....................
}