您的位置:首页 > 其它

Volley(六 )—— 从源码带看Volley的缓存机制

2016-02-17 15:32 351 查看

磁盘缓存DiskBasedCache

如果你还不知道volley有磁盘缓存的话,请看一下我的另一篇博客请注意,Volley已默认使用磁盘缓存

DiskBasedCache内部结构

它由两部分组成,一部分是头部,一部分是内容;先得从它的内部静态类CacheHeader(缓存的头部信息)讲起,先看它的内部结构:

staticclassCacheHeader{
/**缓存文件的大小*/
publiclongsize;

/**缓存文件的唯一标识*/
publicStringkey;

/**这个是与与http请求缓存相关的标签*/
publicStringetag;

/**服务器的返回来数据的时间*/
publiclongserverDate;

/**TTL缓存过期时间.*/
publiclongttl;

/**SoftTTL缓存新鲜度时间.*/
publiclongsoftTtl;

/**服务器还回来的头部信息.*/
publicMap<String,String>responseHeaders;
}

//可以看到,头部类里包含的都是一些基本信息。再来看一下内容部分,父类Cache里面的的Entry:
publicstaticclassEntry{
/**服务端返回数据的主要内容.*/
publicbyte[]data;

publicStringetag;

publiclongserverDate;

publiclongttl;

publiclongsoftTtl;
}


/**缓存文件的大小*/ publiclongsize; /**缓存文件的唯一标识*/ publicStringkey; /**这个是与与http请求缓存相关的标签*/ publicStringetag; /**服务器的返回来数据的时间*/ publiclongserverDate; /**TTL缓存过期时间.*/ publiclongttl; /**SoftTTL缓存新鲜度时间.*/ publiclongsoftTtl; /**服务器还回来的头部信息.*/ publicMap<String,String>responseHeaders; } //可以看到,头部类里包含的都是一些基本信息。再来看一下内容部分,父类Cache里面的的Entry: publicstaticclassEntry{ /**服务端返回数据的主要内容.*/ publicbyte[;'>data; publicStringetag; publiclongserverDate; publiclongttl; publiclongsoftTtl; }"data-snippet-id="ext.e1d417cff682c96e29db82afd6f280ee"data-snippet-saved="false"data-csrftoken="FlyiqKbn-BsUNDlOO1nDS8T5VuTEm1VpwVbg"data-codota-status="done]

可以看到,Entry里面和CacheHeader里有四个参数是一样的,只是Entry里多了data[],data[]就是用来保存主要数据的。看到这你可以有点迷糊,Entry和CacheHeader里为什么要有四个参数一样,先简单说一下原因:volley框架里都用到接口编程,所以实际代码中除了初始化,你只看到cache,而DiskBasedCache是看不到的,所以必须在Entry里先把那些缓存需要用到的参数保留起来,然后具体实现和封装放在DiskBasedCache里。

DiskBasedCache的使用流程

初始化

DiskBasedCache的初始化时在RequestQueue新建时就发生的,可以看Volley.newRequestQueue()的源码:

[code]publicstaticRequestQueuenewRequestQueue(Contextcontext,HttpStackstack,intmaxDiskCacheBytes){
FilecacheDir=newFile(context.getCacheDir(),DEFAULT_CACHE_DIR);

....

//maxDiskCacheBytes为缓存的最大容量,不传就默认为5M
if(maxDiskCacheBytes<=-1)
{
//Nomaximumsizespecified
queue=newRequestQueue(newDiskBasedCache(cacheDir),network);
}
else
{
//Diskcachesizespecified
queue=newRequestQueue(newDiskBasedCache(cacheDir,maxDiskCacheBytes),network);
}

queue.start();
returnqueue;
}

可以看到,磁盘缓存的路径为:context.getCacheDir(),如果maxDiskCacheBytes有传入,就以传入的为准,如果为空:

/**默认的磁盘存放的最大byte*/
privatestaticfinalintDEFAULT_DISK_USAGE_BYTES=5*1024*1024;
...
publicDiskBasedCache(FilerootDirectory){
this(rootDirectory,DEFAULT_DISK_USAGE_BYTES);
}




磁盘默认为5M。所以如果你想设置最大的磁盘缓存值,那么就不能直接向下面那样这样初始化了:

queue=Volley.newRequestQueue(context);

而是需要这样:

queue=Volley.newRequestQueue(context,10*1024*1024);


存放缓存数据

第一次缓存的数据是从哪来的呢,当然是从网上来,看NetWorkDispatcher的run方法里:

@Override
publicvoidrun(){
while(true){
...
try{
....
请求解析http的返回信息
....
if(request.shouldCache()&&response.cacheEntry!=null){
mCache.put(request.getCacheKey(),response.cacheEntry);
request.addMarker("network-cache-written");
}

....
}
}




其中request.getCacheKey()默认为请求的url,response.cacheEntry是Cache.Entry,里面已存放好解析完的httpResponse数据,request.shouldCache()默认是需要缓存,如果不需要可调用request.setShouldCache(false)来去掉缓存功能。

我们把请求和处理的http的返回略过,留下几行关键代码,如果这个请求需要缓存(默认需要)和缓存信息不为空,那么就保存缓存信息。接下来看,DiskBaseCache是怎么保存缓存的:

/**
*把缓存数据Entry写进磁盘里
*/
@Override
publicsynchronizedvoidput(Stringkey,Entryentry){
//判断是否有足够的缓存空间来缓存新的数据
pruneIfNeeded(entry.data.length);

Filefile=getFileForKey(key);
try{
FileOutputStreamfos=newFileOutputStream(file);
//用enry里面的数据,再封装成一个CacheHeader
CacheHeadere=newCacheHeader(key,entry);
//先写头部缓存信息
booleansuccess=e.writeHeader(fos);
if(!success){
fos.close();
VolleyLog.d("Failedtowriteheaderfor%s",file.getAbsolutePath());
thrownewIOException();
}
//成功后再写缓存内容
fos.write(entry.data);
fos.close();
//把头部信息先暂时保存在一个容器里
putEntry(key,e);
return;
}catch(IOExceptione){
}
booleandeleted=file.delete();
if(!deleted){
VolleyLog.d("Couldnotcleanupfile%s",file.getAbsolutePath());
}
}


可以看到,每次写入缓存之前,都先调用pruneIfNeeded()检查对象的大小,当缓冲区空间足够新对象的加入时就直接添加进来,否则会删除部分对象,一直到新对象添加进来后还会有10%的空间剩余时为止,文件引用以LinkHashMap保存。添加时,首先以URL为key,经过个文本转换后,以转换后的文本为名称,获取一个file对象。首先向这个对象写入缓存的头文件,然后是真正有用的网络返回数据。最后是当前内存占有量数值的更新,这里需要注意的是真实数据被写入磁盘文件后,在内存中维护的应用,存的只是数据的相关属性。

从缓存数据里取缓存

我们知道队列创建后就会有一个缓存线程在后台一直运行等待着缓存请求进来,但在等待线程前,会先调用mCache.initialize(),把缓存数据的头部信息放进一个Map类型mEntries里,这样以后要用到就先用mEntries判断,速度更快。

如果请求进来即调用
Cache.Entryentry=mCache.get(request.getCacheKey())
,那我们就看DiskBaseCache。get方法里做了什么:

@Override
publicsynchronizedEntryget(Stringkey){
CacheHeaderentry=mEntries.get(key);
//如果entry不为空,就直接返回
if(entry==null){
returnnull;
}

Filefile=getFileForKey(key);
CountingInputStreamcis=null;
try{
cis=newCountingInputStream(newFileInputStream(file));
CacheHeader.readHeader(cis);//eatheader
byte[]data=streamToBytes(cis,(int)(file.length()-cis.bytesRead));
returnentry.toCacheEntry(data);
}catch(IOExceptione){
VolleyLog.d("%s:%s",file.getAbsolutePath(),e.toString());
remove(key);
returnnull;
}finally{
if(cis!=null){
try{
cis.close();
}catch(IOExceptionioe){
returnnull;
}
}
}
}


从方法里可以看到,先从文件里获得字节数输入流,从中减去头部文件的字节数,最后把真正内容的data[]数据拿到再组装成一个Cache.Entry返回。不得不说,Volley这真是精打细算啊。

从上面的分析可见,cache在做一些基础判断时都会先用到缓存的头部数据,如果确定头部信息没问题了,再真正读写内容,原因是头部数据比较小,放在内存中也不占地方,但处理速度会快很多。而真正的数据内容,可能会比较大,处理的开销也大,只在真正需要的地方读写。

Volley对304的处理

http的304状态码的含义是:


如果服务器端的资源没有变化,则自动返回HTTP304(NotChanged.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。


完整的过程如下:

客户端请求一个页面(A)。

服务器返回页面A,并在给A加上一个Last-Modified/ETag。(Last-Modified为标记此文件在服务期端最后被修改的时间,ETag是这个请求的token)

客户端展现该页面,并将页面连同Last-Modified/ETag一起缓存。

客户再次请求页面A,并将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器。

服务器检查该Last-Modified或ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304和一个空的响应体。

介绍完304,我们接下来来看看volley是怎么运用304来重用缓存的。

Volley对于头部的解析

首先我们来看一下对于response.header的处理,在每一个request里,都必须继承parseNetworkResponse(NetworkResponseresponse)方法,然后在里面用HttpHeaderParser.parseCacheHeaders()解析类来解析头部数据,具体如下:

publicstaticCache.EntryparseCacheHeaders(NetworkResponseresponse){
longnow=System.currentTimeMillis();

Map<String,String>headers=response.headers;

longserverDate=0;
longserverExpires=0;
longsoftExpire=0;
longmaxAge=0;
booleanhasCacheControl=false;

StringserverEtag=null;
StringheaderValue;

headerValue=headers.get("Date");
if(headerValue!=null){
serverDate=parseDateAsEpoch(headerValue);
}

headerValue=headers.get("Cache-Control");
if(headerValue!=null){
hasCacheControl=true;
String[]tokens=headerValue.split(",");
for(inti=0;i<tokens.length;i++){
Stringtoken=tokens[i].trim();
//如果Cache-Control里为no-cache和no-store则表示不需要缓存,返回null
if(token.equals("no-cache")||token.equals("no-store")){
returnnull;
}elseif(token.startsWith("max-age=")){
try{
maxAge=Long.parseLong(token.substring(8));
}catch(Exceptione){
}
}elseif(token.equals("must-revalidate")||token.equals("proxy-revalidate")){
maxAge=0;
}
}
}

headerValue=headers.get("Expires");
if(headerValue!=null){
serverExpires=parseDateAsEpoch(headerValue);
}
serverEtag=headers.get("ETag");
//Cache-ControltakesprecedenceoveranExpiresheader,evenifbothexistandExpires
//ismorerestrictive.
if(hasCacheControl){
softExpire=now+maxAge*1000;
}elseif(serverDate>0&&serverExpires>=serverDate){
//DefaultsemanticforExpireheaderinHTTPspecificationissoftExpire.
softExpire=now+(serverExpires-serverDate);
}

Cache.Entryentry=newCache.Entry();
entry.data=response.data;
entry.etag=serverEtag;
entry.softTtl=softExpire;
entry.ttl=entry.softTtl;
entry.serverDate=serverDate;
entry.responseHeaders=headers;
returnentry;
}


从上面代码可以看出缓存头部是根据Cache-Control和Expires首部,计算出缓存的过期时间(ttl),和缓存的新鲜度时间(softTtl,默认softTtl和ttl相同),如果有Cache-Control标签以它为准,没有就以Expires标签里的内容为准。


需要注意的是:Volley没有处理Last-Modify首部,而是处理存储了Date首部,并在后续的新鲜度验证时,使用Date来构建If-Modified-Since。这与Http1.1的语义有些违背。


Volley对于新鲜度和过期的验证

在使用缓存数据前,Volley会先对验证缓存数据是否过期,是否需要更新等属性,然后一一处理,代码在CacheDispatcher的run方法里:

@Override
publicvoidrun(){
if(DEBUG)VolleyLog.v("startnewdispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//初始化缓存,里面会先把磁盘缓存里的头部数据缓存进内存里,增加处理速度
mCache.initialize();

while(true){
try{
//阻塞线程直到有请求加入,才开始运行
finalRequestrequest=mCacheQueue.take();
request.addMarker("cache-queue-take");

//请求是否取消
if(request.isCanceled()){
request.finish("cache-discard-canceled");
continue;
}

//得到缓存数据entry
Cache.Entryentry=mCache.get(request.getCacheKey());
//如果缓存不存在,就把请求交给网络队列取处理
if(entry==null){
request.addMarker("cache-miss");
mNetworkQueue.put(request);
continue;
}

//如果请求过期,也需要到网络重新获取数据
if(entry.isExpired()){
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}

//到这里就表明缓存数据是可用的,解析缓存
request.addMarker("cache-hit");
Response<?>response=request.parseNetworkResponse(
newNetworkResponse(entry.data,entry.responseHeaders));
request.addMarker("cache-hit-parsed");

//验证缓存的新鲜度
if(!entry.refreshNeeded()){
//新鲜的
mDelivery.postResponse(request,response);
}else{
//不新鲜,虽然把缓存数据分发出去,但还是需要到网络上验证缓存是否需要更新
request.addMarker("cache-hit-refresh-needed");
//请求带上缓存属性
request.setCacheEntry(entry);

response.intermediate=true;

//分发完缓存数据后,将请求加入网络请求队列,判断是否需要更新缓存数据
mDelivery.postResponse(request,response,newRunnable(){
@Override
publicvoidrun(){
try{
mNetworkQueue.put(request);
}catch(InterruptedExceptione){
//Notmuchwecandoaboutthis.
}
}
});
}

}catch(InterruptedExceptione){
//Wemayhavebeeninterruptedbecauseitwastimetoquit.
if(mQuit){
return;
}
continue;
}
}
}


上面代码都已经加了注释,相信不难理解,那我们继续看,网络请求是怎么判断是否需要更新缓存的,在BasicNetwork.performRequest()里:

@Override
publicNetworkResponseperformRequest(Request<?>request)throwsVolleyError{
....
while(true){
...
try{
Map<String,String>headers=newHashMap<String,String>();
//如果请求有带属性,就将etag和If-Modified-Since属性加上
addCacheHeaders(headers,request.getCacheEntry());
httpResponse=mHttpStack.performRequest(request,headers);
StatusLinestatusLine=httpResponse.getStatusLine();
intstatusCode=statusLine.getStatusCode();

responseHeaders=convertHeaders(httpResponse.getAllHeaders());
//如果304就直接用缓存数据返回
if(statusCode==HttpStatus.SC_NOT_MODIFIED){
returnnewNetworkResponse(HttpStatus.SC_NOT_MODIFIED,
request.getCacheEntry().data,responseHeaders,true);
}
.....
returnnewNetworkResponse(statusCode,responseContents,responseHeaders,false);
}catch(SocketTimeoutExceptione){
...
}
}
}


从上面的注释可以看到,如果是返回304就直接用缓存数据返回。那来看NetworkDispatcher的run()里:

publicvoidrun(){
...
NetworkResponsenetworkResponse=mNetwork.performRequest(request);
request.addMarker("network-http-complete");
//如果是304并且已经将缓存分发出去里,就直接结束这个请求
if(networkResponse.notModified&&request.hasHadResponseDelivered()){
request.finish("not-modified");
continue;
}
...
}
}


现在流程比较清晰了,在有缓存的情况下,如果已经过期,但是返回304,就复用缓存。如果不新鲜了,就先将缓存分发出去,然后再进行网络请求,看是否需要更新缓存。

不过眼尖的读者一定有个疑惑,在解析头部数据时,默认不是新鲜度和过期事件是一样的吗?那新鲜度不是一定运行不到吗?确实是这样,我也有这个疑惑,网上也找不到确切的资料来解释这一点。不过按照正常的逻辑,新鲜度时间一定比过期时间短,这样我们就可以根据实际需要更改Volley的源码。例如,我们可以直接把新鲜度的验证时间设为3分钟,而过期时间设为一天,代码如下:

publicstaticCache.EntryparseIgnoreCacheHeaders(NetworkResponseresponse){
longnow=System.currentTimeMillis();
Map<String,String>headers=response.headers;
longserverDate=0;
StringserverEtag=null;
StringheaderValue;
headerValue=headers.get("Date");

if(headerValue!=null){
serverDate=HttpHeaderParser.parseDateAsEpoch(headerValue);
}

serverEtag=headers.get("ETag");
finallongcacheHitButRefreshed=3*60*1000;
finallongcacheExpired=24*60*60*1000;
finallongsoftExpire=now+cacheHitButRefreshed;
finallongttl=now+cacheExpired;

Cache.Entryentry=newCache.Entry();
entry.data=response.data;
entry.etag=serverEtag;
entry.softTtl=softExpire;
entry.ttl=ttl;
entry.serverDate=serverDate;
entry.responseHeaders=headers;
returnentry;
}


然后使用的时候:

publicclassMyRequestextendscom.android.volley.Request<MyResponse>{
...
@Override
protectedResponse<MyResponse>parseNetworkResponse(NetworkResponseresponse){
StringjsonString=newString(response.data);
MyResponseMyResponse=gson.fromJson(jsonString,MyResponse.class);
returnResponse.success(MyResponse,HttpHeaderParser.parseIgnoreCacheHeaders(response));
}
}


这样的话,在3分钟后就不新鲜,24小时后就会过期。

图片的自定义内存缓存

我们使用ImageLoader时会传入一个ImageCache,它是个接口,里面定义了两个方法:

publicinterfaceImageCache{
publicBitmapgetBitmap(Stringurl);
publicvoidputBitmap(Stringurl,Bitmapbitmap);
}


那他们是什么时候使用的呢,可以从开始请求数据ImageLoader.get()方法看起:

publicImageContainerget(StringrequestUrl,ImageListenerimageListener,
intmaxWidth,intmaxHeight){
//请求只能在主线程里,不然会报错
throwIfNotOnMainThread();
//用url和宽高组成key
finalStringcacheKey=getCacheKey(requestUrl,maxWidth,maxHeight);

//从内存缓存里获取数据
BitmapcachedBitmap=mCache.getBitmap(cacheKey);
if(cachedBitmap!=null){
//如果内存不为空,直接返回图片信息
ImageContainercontainer=newImageContainer(cachedBitmap,requestUrl,null,null);
imageListener.onResponse(container,true);
returncontainer;
}

...
//如果为空,就正常请求网络数据,下面用的是ImageRequest取请求网络数据
Request<?>newRequest=
newImageRequest(requestUrl,newListener<Bitmap>(){
@Override
publicvoidonResponse(Bitmapresponse){
//请求成功后,在这个方法里,把图片放进内存缓存中
onGetImageSuccess(cacheKey,response);
}
},maxWidth,maxHeight,
Config.RGB_565,newErrorListener(){
@Override
publicvoidonErrorResponse(VolleyErrorerror){
onGetImageError(cacheKey,error);
}
});
...
}

privatevoidonGetImageSuccess(StringcacheKey,Bitmapresponse){
//把图片放进内存里
mCache.putBitmap(cacheKey,response);
...
}


从上面的代码注释中已经能比较清晰的看出,每次调用ImageLoader.get()方法,会先从内存缓存里先看有没有数据,有就直接返回,没有就走正常的网络流程,先查看磁盘缓存,不存在或过期再去请求网络。图片比普通数据多一层缓存的原因也很简单,因为图片较大,读取和网络成本都大,能用缓存就用缓存,能省一点是一点。

下面来看看具体的流程图



以上就是Volley框架所使用到的所有缓存机制,如有遗漏请留言指出,多谢阅读。

参考链接:

Volley网络请求源码解析——击溃6大疑虑

Last-Modified和If-Modified-Since

Volley源码解析
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐
章节导航