本文主要是介绍Volley的cache之硬盘缓存--DiskBasedCache,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前言
衡量一个框架的优劣,缓存的处理是很重要的指标。这次我将对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.*/@Overridepublic 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.*/@Overridepublic 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 headerbyte[] 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 eventuallythrow 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<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();while (iterator.hasNext()) {Map.Entry<String, CacheHeader> entry = iterator.next();CacheHeader e = entry.getValue();boolean deleted = getFileForKey(e.key).delete();if (deleted) {mTotalSize -= e.size;} else {VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",e.key, getFilenameForKey(e.key));}iterator.remove();prunedFiles++;if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {break;}}if (VolleyLog.DEBUG) {VolleyLog.v("pruned %d files, %d bytes, %d ms",prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);}}
看似很长,实则很优美。主要逻辑在 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
这篇关于Volley的cache之硬盘缓存--DiskBasedCache的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!