上一章我们重点分析了Volley框架之中两种线程的处理流程,以及这两种线程是如何与UI线程进行通信的。
本章我们将分析Volley框架之中的缓存机制。
任何一个网络请求都会存在一定的阻塞延时(哪怕网速再快),而作为一个网络框架,Volley引入了缓存机制,最大程度了减少了这一缺点对用户体验的影响。
通过上一篇文章【进阶android】Volley源码分析——Volley的线程对Volley框架中的缓存线程分析我们知道:如果一个Request对象存在相应的本地缓存实体(Cache.Entry),而无需将该对象添加到网络队列之中,让网络线程进行网络请求。以此来减少网络请求的延迟,加快网络响应的速率。而需要进行网络请求时,在成功的从网络获取到响应后,会将此响应转为Cache.Entry对象存入缓存之中。
在清晰了缓存在Volley框架中的作用(为什么要使用缓存)后,我们就应当考虑什么是缓存?Volley框架中怎样实现缓存机制?很幸运,Volley框架分别用Cache接口和Cache接口默认执行类,回答了这两个问题。
一、Volley框架中的缓存机制
什么是缓存机制?其实前面几篇文章或多或少都有相关的解释和定义。在Volley框架中,缓存机制是一系列缓存实体的集合以及此集合对应的一系列操作。
在回答了缓存机制的定义后,我们从其定义当中又可以延伸出一系列问题:缓存机制缓存的是什么?缓存实体是什么?缓存集合有哪些操作?
而将这些问题放入整个Volley框架、甚至整个Android框架之中,我们有推出以下一些问题:缓存实体与Request对象、Response对象有什么关系?缓存机制和Android提供的一些列缓存算法,例如LruCache又有什么关联?
下面我们从源码的角度入手,尝试着是否能够分析出上述问题的答案。
首先,我们先贴出Cache接口的代码:
public interface Cache {
/**
* Retrieves an entry from the cache.
* //从缓存中获取一个实体
* @param key Cache key
* @return An {@link Entry} or null in the event of a cache miss
*/
public Entry get(String key);
/**
* Adds or replaces an entry to the cache.
* 添加或者替换缓存中的一个实体
* @param key Cache key
* @param entry Data to store and metadata for cache coherency, TTL, etc.
*/
public void put(String key, Entry entry);
/**
* Performs any potentially long-running actions needed to initialize the cache;
* will be called from a worker thread.
* 在工作线程中执行一系列可能耗时的动作,用以初始化缓存
*/
public void initialize();
/**
* Invalidates an entry in the cache.
* 使缓存中的某一个实体失效
* @param key Cache key
* @param fullExpire True to fully expire the entry, false to soft expire
*/
public void invalidate(String key, boolean fullExpire);
/**
* Removes an entry from the cache.
* 从缓存中删除一个实体
* @param key Cache key
*/
public void remove(String key);
/**
* Empties the cache.
* 清空缓存
*/
public void clear();<pre name="code" class="java">...
}
Cache接口定义了六个抽象方法,也就是说定义了六个对缓存的操作;这六种操作分别为增(改)、删、查、清空、初始以及失效某个实体。而这六个操作就回答了上文”缓存机制有哪些操作?“这一问题。
现在我们但看put和get两个方法,发现put方法的第二个入参及get方法的返回类型皆为Entry类型。显然这个Entry类型就是上文所说的缓存实体。
因此就可以回答“缓存机制缓存的是什么?”这一问题,即缓存机制缓存的是Cache.Entry这一缓存实体。
Entry类是Cache接口定义的一个内部类,其代码如下:
public interface Cache {
...
/**
* Data and metadata for an entry returned by the cache.
* 从缓存中返回的数据实体或者元数据实体
*/
public static class Entry {
/** The data returned from cache.从缓存文件中返回的数据 */
public byte[] data;
/** ETag for cache coherency.缓存一致性 */
public String etag;
/** Date of this response as reported by the server.服务器报告该响应的时间 */
public long serverDate;
/** TTL for this record.本记录的生存时间值 */
public long ttl;
/** Soft TTL for this record. 本记录的软件生存时间值*/
public long softTtl;
/** 从服务端接收的不可改变的响应头,不能为空*/
public Map<String, String> responseHeaders = Collections.emptyMap();
/** 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();
}
}
}
Entry类中有两个属性较为重要,一个是responseHeaders,一个是data;我们可以根据HttpHeaderParser类中的静态方法parseCacheHeaders,源码如下:
public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
...
//响应头
Map<String, String> headers = response.headers;
...
Cache.Entry entry = new Cache.Entry();
entry.data = response.data;
...
entry.responseHeaders = headers;
return entry;
}
parseCacheHeaders方法将NetworkResponse对象转换成一个Cache.Entry对象;通过
上一篇文章
【进阶android】Volley源码分析——Volley的线程
可知在BasicNetwork类中的performRequet方法将原始的HttpResponse解析成中间产物NetworkResponse对象,而解析的方式是提取原始响应的响应头参数、响应内容以及响应码,以这三者生成一个新的NetWorkResponse对象。
也就是说NetworkResponse对象中的data就是原始HTTP响应对象中的响应内容,headers就是原始HTTP响应对象中的响应头参数。
根据parseCacheHeaders方法,我们可以这样认为:Cache.Entry对象中的data属性就是原始HTTP响应的响应内容的映射,CacheEntry对象中的responseHeaders就是原始HTTP响应头参数的映射,进一步而言,Cache.Entry就是原始HTTP响应的一种映射(响应内容和响应头参数都有关联)。
从本质上来讲,Volley框架缓存机制缓存的是一个http响应。
另一方面,Request类和Response类都有对Cache.Entry实体的引用;对于Request类而言,Cache.Entry是其缓存在本地的结果的一种体现;而对于Response类,其对象包含一个已经解析了的响应内容(通过Request子类的parseNetworkResponse方法)以及一个原始HTTP的响应内容,而Cache.Entry这个原始HTTP的响应内容的一种映射。
至此,可以通过下图,来回答“缓存实体与Request对象、Response对象有什么关系?”这一问题:
以上大致分析了Volley框架之中的缓存机制是什么;下面继续看看Volley框架是如何实现缓存机制的。
二、缓存机制的实现
在Volley框架之中,默认通过DiskBasedCache类来实现缓存机制。DiskBasedCache主要是通过Disk,也就是磁盘来实现缓存,而具体的表现形式就是不同的文件。通过文件的形式实现缓存,缓存的生命周期比内存缓存的生命周期更长,即便退出应用,这些缓存也不会随之删除。
DiskBasedCache通过文件映射Cache.Entry;并且,DisBasedCache也封装了两者之间的转换。
提及两者之间的转换,就不得不说明DiskBashedCache类中的一个内部类CacheHeader,如果说Cahche.Entry被映射为文件,那么CacheHeader就是这个Cahche.Entry对象的一种摘要。
DiskBasedCache是基于文件来实现缓存机制,故而它是通过IO流的方式来进行存取文件内容的操作;众所周知,IO流是阻塞读写,因此除了必要的操作之外,例如将文件转换成Cache.Entry等;一些用以描述缓存实体的信息则可存入一段内存之中(这些信息例如文件的大小、缓存的生命周期等),这样当需要获取这些信息的时候,就可直接从内存中获取,而不用再通过IO方式从文件中读取了。而这段内存就是CacheHeader对象。
DiskBasedCache间接的通过的CacheHeader来管理文件(Cache.Entry);DiskBasedCache之中有一个LinkedHashMap对象,该对象的元素就是CacheHeader。DiskBasedCache通过LinkedHashMap来管理CacheHeader,实现Lru(Least Recently Used )算法,从来间接管理文件缓存。
通过上文对Cache接口的分析,确定了Cache接口之中缓存的是Cache.Entry对象这一结论,而在具体的Cache接口实现类DiskBasedCached之中,这一结论有一定的变异,在DiskBasedCache之中缓存的是CacheHeader,由CacheHeader指向一个Cache.Entry,也就是一个文件,因为文件是Cache.Entry的一个映射。
如图所示:
因此,在DiskBasedCache类中,缓存和缓存集合是两个不同的概念;缓存囊括了缓存集合和缓存文件,缓存集合则只是CacheHeader的集合。
说了这么多,我们看看CacheHeader的源码:
static class CacheHeader {
/** The size of the data identified by this CacheHeader. (This is not
* serialized to disk. 原始响应内容的长度*/
public long size;
/** The key that identifies the cache entry. */
public String key;
/** ETag for cache coherence. */
public String etag;
/** Date of this response as reported by the server. */
public long serverDate;
/** TTL for this record. */
public long ttl;
/** Soft TTL for this record. */
public long softTtl;
/** Headers from the response resulting in this cache entry. */
public Map<String, String> responseHeaders;
private CacheHeader() { }
/**
* Instantiates a new CacheHeader object
* @param key The key that identifies the cache entry
* @param entry The cache entry.
*/
public CacheHeader(String key, Entry entry) {
this.key = key;
this.size = entry.data.length;
this.etag = entry.etag;
this.serverDate = entry.serverDate;
this.ttl = entry.ttl;
this.softTtl = entry.softTtl;
this.responseHeaders = entry.responseHeaders;
}
/**
* Reads the header off of an InputStream and returns a CacheHeader object.
* 文件转换成CacheHeader
* @param is The InputStream to read from.
* @throws IOException
*/
public static CacheHeader readHeader(InputStream is) throws IOException {
CacheHeader entry = new CacheHeader();
int magic = readInt(is);
if (magic != CACHE_MAGIC) {
// don't bother deleting, it'll get pruned eventually
throw new IOException();
}
entry.key = readString(is);
entry.etag = readString(is);
if (entry.etag.equals("")) {
entry.etag = null;
}
entry.serverDate = readLong(is);
entry.ttl = readLong(is);
entry.softTtl = readLong(is);
entry.responseHeaders = readStringStringMap(is);
return entry;
}
/**
* Creates a cache entry for the specified data.
* CacheHeader到Cache.Entry的转换
*/
public Entry toCacheEntry(byte[] data) {
Entry e = new Entry();
e.data = data;
e.etag = etag;
e.serverDate = serverDate;
e.ttl = ttl;
e.softTtl = softTtl;
e.responseHeaders = responseHeaders;
return e;
}
/**
* Writes the contents of this CacheHeader to the specified OutputStream.
* CacheHeader转换成文件
*/
public boolean writeHeader(OutputStream os) {
try {
writeInt(os, CACHE_MAGIC);
writeString(os, key);
writeString(os, etag == null ? "" : etag);
writeLong(os, serverDate);
writeLong(os, ttl);
writeLong(os, softTtl);
writeStringStringMap(responseHeaders, os);
os.flush();
return true;
} catch (IOException e) {
VolleyLog.d("%s", e.toString());
return false;
}
}
}
通过源码,我们可以得出
CacheHeader与Cache.Entry的区别:Cache.Entry比CacheHeader多了一个data属性,Cache.Entry中该字段用以存储原始响应内容。
而CahceHeader的方法之中,除了构造函数,其他的都是一些转换方法:
文件转换成CacheHeader对象,此方法采用流的方式从文件之中读取内容,赋值给CacheHeader相应的属性;
CacheHeader转换为Cache.Entry;
CacheHeader转换为文件;此时转换的文件不包含原始的响应内容。
分析完DiskBasedCache之中缓存实体的实现,我们接着分析其中缓存的实现。
缓存,其实就是一些列缓存实体的集合,以及这个集合的一些基本操作。在DiskBasedCache之中,通过一个LinkedHashMap定义了这个集合。原型如下:
private final Map<String, CacheHeader> mEntries = new LinkedHashMap<String, CacheHeader>(16, .75f, true);
下面进入集合的操作,DiskBasedCache之中绝大部分的方法都是封装了对缓存集合的操作,我们主要分析六个:
initialize,初始化缓存;
put,存入缓存;
get,从缓存之中获取;
remove,从缓存之中删除;
clear,清空缓存;
pruneIfNeeded,裁剪缓存。
方法的代码如下:
缓存的初始化主要有两方面;public synchronized void initialize() { if (!mRootDirectory.exists()) {//如果根目录不存在,创建根目录 if (!mRootDirectory.mkdirs()) { VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath()); } return; } File[] files = mRootDirectory.listFiles(); if (files == null) { return; } //遍历所有的文件,将文件转换为CacheHeader对象,并将其添加到缓存集合之中 for (File file : files) { FileInputStream fis = null; try { fis = new FileInputStream(file); CacheHeader entry = CacheHeader.readHeader(fis); entry.size = file.length(); putEntry(entry.key, entry);//添加到缓存集合之中 } catch (IOException e) { if (file != null) { file.delete();//如果存在异常则删除异常文件 } } finally { ... } } }
因为DiskBasedCache是一个基于文件的缓存,所以其对应一个根目录来存放这些文件,因而初始化后缓存时需要确保这个根目录存在,如果不存在则创建之。
确定了根目录之后,就遍历根目录之下的文件,为每一个文件生成一个摘要,即CacheHeader对象,再将其存放到缓存集合之中。由于是遍历根目录之中的所有文件,并且采用IO流的方式获取文件内容,故而initialize方法可能会很耗时,故而一般不会再UI线程之中运行,Volley框架之中通过缓存线程来运行。 2、put(String key,Entry entry)
方法代码如下:
添加缓存的方法也较为简单,先将需要缓存的内容(响应摘要和原始响应内容)分别写入缓存文件之中,再将摘要(CacheHeader)添加到缓存集合之中。<span style="font-size:18px;">public synchronized void put(String key, Entry entry) { pruneIfNeeded(entry.data.length); File file = getFileForKey(key);//根据key找到缓存文件 try { FileOutputStream fos = new FileOutputStream(file); CacheHeader e = new CacheHeader(key, entry);//通过Cache.Entry生成一个摘要(CacheHeader) e.writeHeader(fos);//将摘要写入缓存文件之中 fos.write(entry.data);//将</span><span style="font-size:18px; font-family: Consolas; widows: auto;">原始响应内容</span><span style="widows: auto; font-family: Consolas;">写入缓存文件之中</span><span style="font-size:18px;"> fos.close(); putEntry(key, e);//将摘要添加到缓存集合之中 return; } catch (IOException e) { } ...... }</span>
3、get(String key)
方法代码如下:
public synchronized Entry get(String key) { CacheHeader entry = mEntries.get(key);//获取摘要 ...... File file = getFileForKey(key);//获取文件 CountingInputStream cis = null; try { cis = new CountingInputStream(new FileInputStream(file)); //eat header文件的具体内容在摘要之后,所有先读取摘要,将当前读取位置移动到摘要之后 CacheHeader.readHeader(cis); //当前位置移动到摘要之后,读取原始响应内容 byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead)); return entry.toCacheEntry(data);//根据读取的原始响应内容,结合缓存摘要,生成Cache.Entry对象 } catch (IOException e) { VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); remove(key);//发生异常则删除异常缓存 return null; } finally { ...... } }
get方法的逻辑也较为简单;总体而言,是要通过CacheHeader(摘要)和原始响应内容两者一起结合为一个Cache.Entry对象;其中CacheHeader来至于缓存集合之中,
原始响应内容则通过IO方式从缓存文件之中读取。
需要注意一点,要读取缓存文件之中的原始响应内容,就必须从文件中原始响应内容第一个byte的位置开始读取,因而在读取原始响应内容之前,需要调用CacheHeader.readHeader(cis)方法将文件之中原始响应内容前面的摘要内容全部读取一遍,这样才能保证input流当前读取位置为文件中原始响应内容第一个byte所在的位置。
4、remove(String key)
该方法的逻辑极为简单,就不罗列其源码了。
该方法,首先根据key找到对应的缓存文件,将其删除;在删除缓存集合之中的缓存摘要。
5、clear()
该方法的逻辑极为简单,就不罗列其源码了。
该方法,首先清空根目录下方所有的文件,再清空缓存集合。
6、pruneIfNeeded(int neededSpace)
DiskBasedCache使用了LRU的模式来维持缓存(注意是缓存,而非缓存集合)的大小(缓存的大小是指所有缓存文件中原始响应内容的长度之和),确保缓存的大小不会超过设定的最大值;如果超过,则删除最近最少使用的缓存。
DiskBasedCache类中有一个变量mMaxCacheSizeInBytes用来限制缓存的大小,默认情况下,缓存的大小被限制为不超过5M,这个值开发人员可以自行设置。一旦缓存的大小超出了该值,则会调用pruneIfNeed方法裁剪集合。
pruneIfNeeded方法代码如下:
private void pruneIfNeeded(int neededSpace) {
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
//未超过缓存最大值限制,直接返回
return;
}
......
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
//遍历缓存集合
while (iterator.hasNext()) {
Map.Entry<String, CacheHeader> entry = iterator.next();
//删除缓存集合头部CacheHeader对象对应的缓存文件,
CacheHeader e = entry.getValue();
boolean deleted = getFileForKey(e.key).delete();
if (deleted) {
mTotalSize -= e.size;//删除头部元素后当前缓存的大小
} else {
......
}
iterator.remove();//删除缓存集合头部CacheHeader对象
......
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
//mTotalSize:当前缓存的大小
//neededSpace:即将添加到缓存文件之中原始响应内容的大小
//mMaxCacheSizeInBytes:缓存最大值限制
//HYSTERESIS_FACTOR:缓存最高水准线因子(水准线与mMaxCacheSizeInBytes的比值,其默认值为0.9)
break;
}
}
......
}
DiskedBasedCache类中,一般在添加缓存(put方法)时,调用此方法,判断算上即将添加的Cache.Entry后是否超过最大限制,如果超过,则进行缓存集合遍历,删除缓存集合头部CacheHeader对象,及其对应的缓存文件;然后在以删除缓存文件后的缓存大小为标准,算上即将添加的Cache.Entry大小,再次判断是否超过最大限制(的水准线)。注意此时判断的是水准线,而非最大限制。
至此我们就将DiskBasedCache类中的操作分析完毕。
结合DiskedBasedCache类中的属性和操作,我们就可以明白Volley框架中默认的缓存机制是如何实现的。最后在做一个总结:
缓存机制:一系列缓存实体的集合以及此集合对应的一系列操作。
缓存:一系列缓存实体的集合。
DiskBasedBache中的缓存:缓存摘要集合(简称缓存集合)+缓存文件集合。
至此,Volley框架里的缓存,我们就分析结束了。
当然由于本人自身水平所限,文章肯定有一些不对的地方,希望大家指出!