前言
衡量一个框架的优劣,缓存的处理是很重要的指标。这次我将对Volley的硬盘缓存DiskBasedCache从源码的角度进行解析。
下面先对DiskBasedCache的原理做简要介绍,开个头,然后根据简介做源码分析。
缓存原理
在说缓存原理之前,要说一下缓存的数据怎么来的。
第一步:
当NetWorkDispatcher的run方法开始执行(NetWorkDispatcher是Thread类的子类,后面会写文章介绍),进入循环,从网络请求队列中取出一个请求对象,执行网络请求。
第二步:
将从服务器得到的数据转换为Response对象(此对象代表一个网络响应)。
第三步:
根据 请求对象是否要求缓存(在新建Request的时候设置的值)来决定是否将响应数据写入缓存中。
以上三步得到缓存数据。缓存包括: 网络响应的响应正文和头信息。
得到缓存数据以后就会调用Cache类或者其子类的put方法将缓存信息写入SD卡(这里可以看出,Volley并没有做内存的缓存而是直接写入到磁盘文件)。调用get方法从SD卡取出缓存数据。
知道一点音频知识的都知道,在音频文件中会有该音频的头部信息,用来描述该音频的一些属性。在Volley中,为了描述缓存文件,google攻城狮也将缓存文件的一些重要属性—缓存文件的大小,缓存对应的URL,服务器的响应时间,网络延迟和缓存的新鲜度作为头信息组成CacheHeader对象写入到缓存文件中,并将响应正文写入CacheHeader后面,组成一个缓存文件。
在取缓存的时候,根据内存中的CacheHeader对象(缓存文件属性的封装类)的map集合(此map的key为待请求的URL,Value是CacheHeader对象)判断此请求对象是否有缓存。如果有缓存的话,将缓存读出来,封装成Entry(缓存属性和数据的封装类),从而恢复成response对象。
以上一写一读就完成了缓存的写入和取出操作。
源代码解析
介绍完了以上的原理,接下来就是代码实现了。我以方法作为单位,对DiskBasedCache源码进行解析。
put(string,entry)写缓存
此方法主要用来将缓存写入到SD卡,并在内存中加入该缓存文件的头信息(CacheHeader)、代码如下:
/**
* Puts the entry with the specified key into the cache.
*/
@Override
public synchronized void put(String key, Entry entry) {
pruneIfNeeded(entry.data.length);
File file = getFileForKey(key);
try {
FileOutputStream fos = new FileOutputStream(file);
CacheHeader e = new CacheHeader(key, entry);
e.writeHeader(fos);
fos.write(entry.data);
fos.close();
putEntry(key, e);
return;
} catch (IOException e) {
}
boolean deleted = file.delete();
if (!deleted) {
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
}
}
方法体的第一行代码的作用是进行 缓存的管理。这一点下面会介绍,先跳过。
接下来,根据请求Url新建缓存文件。然后使用字节流将数据写入该文件中。注意代码将写头部信息和写数据是分开的,这里是因为头部信息是有一定的结构,必须按照头信息的格式写入文件。写完之后,将代表缓存信息的CacheHeader放入内存方便以后对缓存的检索。如果在写文件中遇到异常,删除缓存文件。如果删除不成功,打Log。其实代码很容易理解。其中一些小点,比如 头信息怎么写的?会在接下来说明。
get(string)读缓存
写的逻辑大家都已经清晰了,读逻辑是写逻辑的逆过程。先上代码:
/**
* Returns the cache entry with the specified key if it exists, null otherwise.
*/
@Override
public synchronized Entry get(String key) {
CacheHeader entry = mEntries.get(key);
// if the entry does not exist, return.
if (entry == null) {
return null;
}
File file = getFileForKey(key);
CountingInputStream cis = null;
try {
cis = new CountingInputStream(new FileInputStream(file));
CacheHeader.readHeader(cis); // eat header
byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
return entry.toCacheEntry(data);
} catch (IOException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} finally {
if (cis != null) {
try {
cis.close();
} catch (IOException ioe) {
return null;
}
}
}
}
首先根据内存中的头信息,判断是否有该key的缓存。有的话,进入if后面的语句:
首先按照put方法中得到文件的做法,来得到缓存文件。接着用一个字节输入流的包装类CountingInputStream读取缓存文件,此类有一个功能–记住已经读取的字节数,从而方便的读取头信息这类有结构的数据(很赞的想法)。将读取到的头信息和读取到的数据组成entry对象返回。与put过程一样,这里也会涉及到头的处理,包括缓存的校验。接下来会说。
CacheHeader缓存文件头信息类
该类封装了 缓存文件的大小,缓存对应的Url,服务器响应的日期,网络延迟,缓存文件的新鲜度,响应的头信息。
该类包含了三个重要方法:
readHeader(InputStream is)读头信息
代码如下:
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;
}
在方法体的第三行,验证输入流对应的缓存文件的合法性。如果读出的magic不等于写入的magic,终止读写,如果合法,从缓存文件中读出结构数据,组成对象返回。
writeHeader(OutputStream os)
与上面方法相反,这里将maic和一些属性信息写入到缓存文件。注意写文件也是按照结构来写。
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;
}
}
toCacheEntry(byte[] data)方法
此方法用于将头信息和数据信息封装成Entry(Cache的单位数据)返回,代码很简单。
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;
}
以上就是关于CacheHeader的介绍。
了解完以上内容,基本上对DiskBasedCache掌握了60%-70%。还有一些很好的方法,接下来继续解析。
pruneIfNeeded(int neededSpace)压缩缓存
说不上压缩,只是不知道如何翻译 prune.这个方法判断 待写入文件 写入之后是否会超出 缓存的最大值(默认5M)。如果超出,就删除掉之前的缓存数据。代码也好懂,如下:
private void pruneIfNeeded(int neededSpace) {
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
return;
}
if (VolleyLog.DEBUG) {
VolleyLog.v("Pruning old cache entries.");
}
long before = mTotalSize;
int prunedFiles = 0;
long startTime = SystemClock.elapsedRealtime();
Iterator
看似很长,实则很优美。主要逻辑在 cacheHeader组成的map的遍历,然后找到头信息所指的缓存文件,删除之。
initialize() 缓存初始化
在缓存处理线程启动的时候,先加载本地已有的缓存文件的头信息到内存方便检索。就是这个作用。代码很清晰:
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;
}
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 {
try {
if (fis != null) {
fis.close();
}
} catch (IOException ignored) { }
}
}
}
总结
到此为止,DiskBasedCache大部分都介绍过了,还有一些方法,如:
readInt 和writeInt
readLong和 writeLong
readString和writeString
readStringStringMap和writeStringStringMap
这些方法主要用于头信息读写有结构的数据,其中夹杂着一些自定义的位运算。
就像方法的代码一样,单个代码很好懂,代码写的很优雅,简洁(写简洁易懂的代码也是一门功课)。既然代码不是难点,那么理清楚方法之间的逻辑关系,就成为要下功夫的地方。读源代码不一定要一遍看懂,首先根据方法名知道方法大概的用途,然后将各个方法的调用关系搞清楚这个类是怎么工作的,最后再一行一行的读代码。这样下来,至少得几遍。还有就是 方法的修饰符 如 public private protected和类的修饰符 static等也成为我们理解的垫脚石。
转载:http://blog.csdn.net/yuan514168845/article/details/49665043